Skip to content

feat(006): inventory-driven vulnerability check via the Cisco PSIRT API (caia --api) - #9

Merged
Ali Bahaloo (alibahaloo) merged 5 commits into
mainfrom
006-api-inventory-check
Jul 25, 2026
Merged

feat(006): inventory-driven vulnerability check via the Cisco PSIRT API (caia --api)#9
Ali Bahaloo (alibahaloo) merged 5 commits into
mainfrom
006-api-inventory-check

Conversation

@alibahaloo

Copy link
Copy Markdown
Collaborator

Adds caia --api, which inverts the tool's question. Instead of "given this advisory, which firewalls are affected?" it asks "given my inventory, which firewalls does Cisco list as vulnerable, and to what?". Verdicts come from Cisco's published per-platform/per-release data, so the mode needs no advisory URL and makes no AI call. Output is one self-contained, timestamped HTML report per run.

Implements specs/006-api-inventory-check — all 51 tasks — from BRD-003.

Why the negative verdict drove the design

A feasibility spike against the team's real 70-row inventory found 15 rows Cisco cannot answer at all — end-of-support releases purged from its catalogue, and model/release pairings Cisco never shipped (ASA 9.18 on 5500-X hardware; FTD 7.4.0 on an ISA-3000) — and zero rows that were genuinely clean. Those 15 return the same "nothing found" as a fully patched device would.

So Not impacted is assigned only when Cisco positively confirms both that it recognises the release and that the release runs on that model. Every other path — unmapped model, unreconcilable platform name, 429, unparseable body, a device not reached during a partial run — falls to Needs review with a specific, correctable reason. Local matching heuristics can only ever reduce Needs review; none can manufacture a Not impacted.

Without that rule this feature would have reported 15 firewalls safe, including DCVPNCORE01 and DCVPNCORE02.

What's new

Module Concern
matching.py Pure, I/O-free release normalisation, model→platform mapping, catalogue indexing, and the ten-row verdict table. Owns the safety property; exhaustively unit-testable with no network.
psirt.py Token acquisition, quota-paced requests, three endpoints, status→outcome classification. The only module that talks to Cisco's advisory API.
html_report.py Single self-contained report — inline CSS/JS, every Cisco value escaped, filter/sort/collapse working offline.

Plus: --config now captures and verifies Cisco credentials; install-test.sh gained --live, --inventory and inventory-mode checks.

Verified live, not just mocked

Run against the real 70-row inventory: 70 firewalls collapse to 27 checks, returning 55 Impacted / 0 Not impacted / 15 Needs review — matching the spike's prediction exactly, with all 15 correctly categorised. 1,582 advisory entries, every one carrying a working Cisco link and a fixed release.

tools/install-test.sh passes in a clean python:3.9-slim container: ALL CHECKS PASSED.

Tests: 68 → 236, all green under CI's unittest discover -s tests.

Constitution compliance (v2.2.0)

  • I. Standard-Library-First — PASS. No new runtime dependency. urllib, json, html.escape, re. A template engine and a CDN table-sort library were both considered and rejected.
  • II. Cross-Platform Parity — PASS. Per-keystroke secret masking deliberately not implemented; it would need termios/msvcrt and per-platform handling of backspace, Ctrl-C and bracketed paste. The non-echoing prompt plus a captured-length confirmation delivers the actual assurance on every terminal.
  • III. CLI-First — PASS. Fully flag-driven; --dry-run exercises the flow with no network; exit codes meaningful (0 for a completed run incl. all-Needs review, non-zero for missing/rejected credentials, unusable inventory, unwritable output, mode conflict; 130 on interrupt).
  • IV. Conservative & Traceable (NON-NEGOTIABLE) — PASS, and this principle drove the design. Impacted is framed as "listed as affected", not "exploitable". No release substitution and no platform-agnostic fallback: asking about a release the device isn't running would be a guess. Every verdict traces to Cisco's published data via publicationUrl.
  • V. Secrets Hygiene & Data Locality — PASS. Credentials live only in the existing per-user config (chmod 600) or the environment; never logged, printed, or written to a report or filename, including in verification failure messages. Only the derived (family, platform, release) triple is sent to Cisco — never a firewall name — a strictly smaller disclosure than the AI endpoint already receives.
  • Technology & Data Constraints — PASS as amended. apix.cisco.com and id.cisco.com were reviewed and admitted in the 2.1.0 → 2.2.0 amendment (cb6938a), along with the PSIRT API as source data and impact_report_*.html as an output.
  • Quality Gates — PASS. Matching logic has tests; README and docs/index.html updated in this PR and mutually consistent; VERSION → 2.3.0 (MINOR).

Reviewer notes — four deliberate deviations

  1. inventory.py was modified, though the plan said "reused unchanged". FR-024 (reproduce the inventory's original columns) and US3 (sort by priority) need data the loader dropped. Extended additively with priority and raw; nothing reads the old shape positionally, and advisory mode is byte-identical.
  2. --config no longer rejects an empty FueliX key. The old retry loop would force an --api-only user to invent one. Deliberate behaviour change, specified in contracts/config-cli.md.
  3. tests/ is now a package so modules can share fixtures. CI's unittest discover -s tests works unchanged.
  4. --config returns non-zero when Cisco rejects credentials. In a single non-interactive pass there's nothing to loop on; it reports Cisco's reason, preserves previously-saved values, saves other settings, and points back at --config. An interactive re-entry loop is a reasonable follow-up.

Two known limitations, disclosed in every report

  • Cisco's data holds no advisories published before January 2022 for ASA/FMC/FTD/FXOS. An Impacted list is not exhaustive and Not impacted is narrower than it appears. Advisory mode remains available for older advisories.
  • A verdict does not know whether a vulnerable feature is enabled.

Also worth knowing: /platforms lags /OS_version/OS_data, so Secure Firewall 200/1200/4200/6100 have no queryable alias yet. Devices on that hardware resolve to Needs review. The catalogues are fetched per run, so the gap closes on Cisco's schedule with no release from us.

Not covered

Quickstart Scenarios E–G (the interactive --config prompt flows) are covered by unit tests but were not exercised against a real terminal, since that would overwrite a developer's stored config. Worth one manual pass before merge:

tools/install-test.sh --shell

🤖 Generated with Claude Code

Ali Bahaloo (alibahaloo) and others added 5 commits July 25, 2026 00:30
…T API

Planning artifacts only — no runtime code changes.

Adds `brds/003-api-inventory-check.md` (BRD-003) defining a `caia --api`
mode that inverts the current question: instead of "given this advisory,
which firewalls are affected?", it asks "given my inventory, which
firewalls does Cisco list as vulnerable, and to what?". Verdicts come
from Cisco's published per-platform/per-release data, so the mode needs
no advisory URL and makes no AI call. Output is a self-contained,
timestamped HTML report per run.

Grounded in a feasibility spike against the real inventory: 70 rows
collapse to 27 distinct checks, and 15 rows proved unanswerable (releases
Cisco has purged as end-of-support, and model/release pairings Cisco
never shipped). That drove the three-verdict model — Impacted / Not
impacted / Needs review — where "Not impacted" requires Cisco to
positively confirm both the release and the platform, so a data-quality
problem can never be read as a clean bill of health.

Adds `specs/006-api-inventory-check/` with the derived spec (5
prioritized user stories, 57 functional requirements, 20 success
criteria) and its quality checklist, and points `.specify/feature.json`
at the new feature directory.

Two governance items deliberately deferred to /speckit-plan:
  - the mode contacts Cisco auth/advisory hosts the analyzer does not
    contact today, which Technology & Data Constraints make a reviewed
    change requiring a constitution amendment;
  - this mode labels an undecidable device "Needs review" while
    Principle IV and the existing report say "Indeterminate" — align
    the vocabulary or record the divergence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Planning artifacts and a governance amendment — no runtime code changes.

Constitution 2.1.0 -> 2.2.0 (MINOR), applied as a prerequisite of spec 006:

  - Principle IV: the insufficient-data result must be labelled undecided
    rather than specifically "Indeterminate", with both that label and
    "Needs review" named as conforming for their respective reports. The
    AI-normalization clause is scoped to modes that use AI.
  - Principle IV also now states the positive-confirmation rule
    explicitly: "not affected" must rest on affirmation from Cisco's data,
    never on the mere absence of a result, and an undecided device must
    never be presented as unaffected. Previously implicit; made explicit so
    the failure mode the spike found cannot be reintroduced.
  - Networking: admits apix.cisco.com (advisory data, catalogues) and
    id.cisco.com (access token), with a new constraint that only the
    derived family/platform/release triple may be sent — never a firewall
    name or other inventory identifier.
  - Source of truth: admits Cisco's PSIRT openVuln API, and requires the
    platform/release catalogues be retrieved from Cisco rather than
    hardcoded.
  - Inputs/outputs: records impact_report_*.html, and makes
    single-file/offline rendering and never-overwrite constitutional.

Phase 0/1 artifacts for spec 006:

  research.md      15 decisions, each verified against the live API rather
                   than taken from documentation. Two findings the docs do
                   not cover: the API separates "no advisories" (404) from
                   "unknown release" (406) by status code where the
                   reference describes one conflated 200; and /platforms
                   disagrees with /OS_version/OS_data on platform display
                   names (FPRNGFW), so name equality cannot carry a safety
                   decision.
  data-model.md    9 entities, the 10-row outcome table that is the single
                   source of verdict truth, and the run/exit-status map.
  contracts/       caia --api CLI, extended caia --config CLI, the external
                   Cisco API contract, and the HTML report contract.
  quickstart.md    Validation scenarios A-K plus the fixture-inventory
                   requirement; Scenario B is the gate.

All Constitution Check gates pass unconditionally now that the amendment
is applied. Complexity Tracking is empty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
51 tasks across 8 phases, organized by user story so each is an
independently testable increment. Planning artifact only — no runtime
code changes.

  Phase 1  Setup: green baseline, gitignore, fixtures            (4)
  Phase 2  Foundational: matching, API client, config, CLI      (13)
  Phase 3  US1 (P1) assess whole inventory — MVP                 (9)
  Phase 4  US2 (P1) specific reasons per unanswerable device     (3)
  Phase 5  US3 (P2) actionable report                            (5)
  Phase 6  US4 (P2) credential setup and verification            (5)
  Phase 7  US5 (P3) provenance and coverage disclosure           (4)
  Phase 8  Polish: cross-cutting paths, docs, validation         (8)

Notes on the breakdown:

  - Tests are marked not-optional. The Development Workflow gate
    requires tests for inventory-matching changes, and this feature is
    entirely inventory matching, so the Tests note cites the gate rather
    than leaving it to preference.

  - MVP is US1 + US2, not US1 alone. US1 is safe on its own — it never
    labels an unanswerable device Not impacted — but on the real
    inventory it would leave 15 of 70 rows carrying an undifferentiated
    "Needs review", which reads as noise. US2 is a 3-task increment that
    turns those into a correctable worklist.

  - T035 is called out as a deliberate behaviour change: it removes the
    retry loop in config.py that currently rejects an empty FueliX key,
    without which an --api-only user cannot complete configuration.

  - The assertion that no unanswerable device is ever labelled Not
    impacted is threaded through T009, T026, T029 and T050 — unit, flow,
    real-inventory cases, and the quickstart gate.

The constitution amendment this feature required is already applied
(2.2.0, cb6938a) and is deliberately absent from the task list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds `caia --api`, which inverts the tool's question: instead of "given
this advisory, which firewalls are affected?", it asks "given my
inventory, which firewalls does Cisco list as vulnerable, and to what?".
Verdicts come from Cisco's published per-platform/per-release data, so
the mode needs no advisory URL and makes no AI call. Output is one
self-contained, timestamped HTML report per run.

New modules (standard library only, no new dependency):

  matching.py      Pure, I/O-free release normalisation, model->platform
                   mapping, catalogue indexing, and the ten-row verdict
                   table. Owns the feature's safety property, and is
                   exhaustively unit-testable without network.
  psirt.py         Token acquisition, quota-paced requests, the three
                   endpoints, and status->outcome classification. The
                   only module that talks to Cisco's advisory API.
  html_report.py   Single self-contained report: inline CSS/JS, every
                   Cisco-supplied value escaped, filter/sort/collapse
                   working offline.

The safety property (Constitution IV): a device is called Not impacted
only when Cisco positively confirms both that it recognises the release
and that the release runs on that model. Every other path — unmapped
model, unreconcilable platform name, 429, unparseable body, unreached
device in a partial run — falls to Needs review with a stated reason.
Local heuristics can only reduce Needs review; none can manufacture a
Not impacted.

Verified live against Cisco with the team's real 70-row inventory: 70
firewalls collapse to 27 checks, returning 55 Impacted / 0 Not impacted
/ 15 Needs review — matching the feasibility study's prediction exactly.
All 15 unanswerable rows are correctly categorised (releases Cisco has
purged as end-of-support, and model/release pairs Cisco never shipped),
including DCVPNCORE01 and DCVPNCORE02, which a binary status would have
reported as safe.

Also in this change:

  - `caia --config` gains the two Cisco prompts: client ID shown as
    entered (an identifier, not a secret), secret never displayed but
    followed by a captured-length confirmation so a truncated paste is
    obvious. It then verifies the credentials against Cisco, keeping
    "rejected" distinct from "unreachable" so a proxy outage never
    blocks setup.
  - Every `--config` prompt is now skippable. This removes the retry
    loop that rejected an empty FueliX key: an --api-only user must not
    be forced to invent one. Deliberate behaviour change, not a
    regression.
  - inventory.py gains `priority` and `raw` (original header -> value)
    so the report can reproduce the user's own columns and sort by
    priority. Additive; nothing reads the old shape positionally.
  - tests/ became a package so modules can share fixtures. CI's
    `unittest discover -s tests` still works unchanged.
  - VERSION 2.2.0 -> 2.3.0 (MINOR: new user-facing capability).

Advisory mode is untouched: cisco.py, fuelix.py, report.py and
version.py are byte-identical, and `caia --url` behaves exactly as
before.

Tests: 68 -> 234, all passing under the CI command. Covers all ten rows
of the verdict table, the response classifier including the documented-
but-unobserved conflated 200 shape, report escaping against advisory
summaries carrying raw HTML, credential hygiene across every failure
path, and end-to-end runs including partial, interrupted and dry runs.

Implements specs/006-api-inventory-check (all 51 tasks).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-run accounting

install-test.sh gains what live testing of `caia --api` needs, rather
than a second script duplicating its Docker plumbing:

  --live            install the latest PUBLISHED GitHub release (what a
                    real user gets today), resolved from the releases
                    API. Warns when the working tree is ahead, since
                    unreleased work is absent in that container.
  --git [REF]       unchanged: a specific branch/tag/commit.
  (default)         unchanged: the current working tree.
  --inventory FILE  mount your own .xlsx read-only instead of the
                    generated sample.

The generated sample inventory grew from one row to eighteen, chosen to
exercise every path the inventory-driven check can take — parenthetical
and build-suffixed releases, an /ASDM annotation, ASAv folding, FMC
(no platform alias), a genuinely clean device, and one row per
Needs-review reason. It is synthetic: no real fleet data enters the
repository, and unlike a real inventory it can demonstrate the
Not impacted state.

Automated checks now cover inventory mode where credentials are not
required: mutual exclusion with --url, the credential gate, --dry-run
resolving offline, and dry-run writing no report. They are skipped with
a clear message on versions predating --api, so --live keeps working
against 2.2.0.

Also fixes a real defect the container run surfaced. The dry run listed
every device as a question that would be asked, so a blank release
appeared as `ftd/ISA3000/?`, an out-of-scope family as
`switch/-/17.9.4`, and an unmappable model as `asa/-/...` — visually
identical to FMC, which genuinely needs no platform. It reported 16
checks where a real run would make 12. It now uses the same
resolve_device logic as a real run and reports the two groups
separately, with alias validation skipped (and that caveat stated)
because it has no live catalogue offline.

Tests: 234 -> 236. Verified end to end by tools/install-test.sh in a
clean python:3.9-slim container: ALL CHECKS PASSED.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@alibahaloo Ali Bahaloo (alibahaloo) added the enhancement New feature or request label Jul 25, 2026
@alibahaloo
Ali Bahaloo (alibahaloo) merged commit c9e4ea3 into main Jul 25, 2026
2 checks passed
@alibahaloo
Ali Bahaloo (alibahaloo) deleted the 006-api-inventory-check branch July 25, 2026 17:30
Ali Bahaloo (alibahaloo) added a commit that referenced this pull request Jul 28, 2026
….0) (#16)

* chore: ignore uv.lock — no install path consumes it

`uv run` in a checkout writes a lockfile that nothing here reads. Every
documented path is `uv tool install` (which resolves dependencies itself)
or `python -m unittest`; no `uv sync` or `uv run` appears in CONTRIBUTING,
README, or the workflows.

Committing it would also work against the packaging intent: this package is
both the `caia` command and an importable library, and pyproject pins loosely
on purpose so consumers resolve their own versions. A committed lockfile
would imply a pinned set neither install path honours.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(008): BRD-005 and spec — bundle-scoped impact reporting

Adds the requirements for scoping a run to a Cisco Event Response bundle and
reporting one row per advisory in that bundle, rather than one row per device.

Feasibility was verified against the live sources using ERP-75736 and the
70-row reference inventory, not read from documentation:

- a bundle page yields its advisory identifiers from the links in its table;
  they are absent from the visible text, so reading the text alone fails
- `advisoryId` and `firstPublished` are already in the per-device payload the
  tool retrieves, so filtering an assessment costs no extra requests
- an advisory impacting nothing appears in no device's result, so its metadata
  needs an individual lookup (1 of 25 here; worst case the bundle size)
- Cisco publishes no bundle endpoint (HTTP 404), hence reading the page
- 24 of the 25 advisories touch at least one device, from 0 to 50 each

Two decisions were escalated rather than defaulted, and are recorded with
their reasoning in the spec's Clarifications:

- the device is the unit of count, with a per-row rollup by affected release.
  A release-based denominator cannot close FR-025's accounting, because an
  undetermined device often has no release Cisco recognises.
- rows are scoped to the families an advisory covers, with whole-run totals
  stated once. A single whole-inventory denominator would emit ~375 mostly
  meaningless undetermined markers across 25 rows, training readers to
  disregard the marker Principle IV depends on.

The load-bearing constraint throughout: a device whose assessment reached no
conclusion has no advisory list at all, so it can neither be asserted as
impacted nor excluded as clean. FR-024 through FR-028 exist for that, and
FR-027b fixes the direction of the guarantee — family scoping may only narrow
which rows an undetermined device appears in, never suppress an impacted
device or manufacture an unaffected one.

Planning gate: fetching a bundle page adds sec.cloudapps.cisco.com as an
endpoint the analyzer contacts, which Technology & Data Constraints designate
a reviewed change requiring a constitution amendment before implementation.

Docs only — no product code changes, so VERSION is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(008): plan, research, data model and contracts

Phase 0 and Phase 1 output for bundle-scoped impact reporting. Fifteen research
decisions, all three items the spec deferred to planning now resolved.

What the research changed:

- No new dependency. An earlier curl timeout suggested the bundle page might
  need a browser engine; it does not — urllib fetched it in 0.54s on first
  attempt. stdlib HTMLParser matching on href and a lexical scan both return
  exactly the 25 identifiers. The parser is chosen regardless: an advisory id
  mentioned in prose rather than linked would silently widen scope, and scope
  is the only thing the page is trusted for.
- The publication-date cross-check floated in the spec is rejected. The date
  window returns 27 advisories for this bundle's date — the 25 plus two
  unrelated same-day publications — so it cannot distinguish a missed parse
  from a same-day publication the bundle legitimately excludes. A check that
  cannot fail cleanly is worse than none; the advisory count shown to the user
  before the run is the honest check.
- Family derivation costs nothing: productNames is already in the per-device
  payload, so both derivation sources are free. Verified on two advisories.
- Added cost is ~3s on a 72s run (0.54s page fetch plus one metadata lookup
  for the single advisory in this bundle that impacts nothing), inside SC-008.

Module split mirrors the roles the package already has, so the Principle VI
boundary needs no new reasoning: erp.py does I/O like psirt, bundle.py is pure
like matching, bundle_report.py renders like html_report. erp and bundle join
the shared surface, because the ERP parse and family derivation are exactly the
fragile logic a second consumer would otherwise reimplement.

The data model's worked example was verified arithmetically against the live
assessment: the per-row invariant (impacted + unaffected + undetermined ==
population) closes on all four sampled rows.

Constitution Check is a CONDITIONAL pass. Two gates depend on an amendment
(3.1.0 -> 3.2.0, MINOR) carried as a blocking prerequisite of the first
implementation task: admit sec.cloudapps.cisco.com; state the scope-selector
boundary that keeps "no scraped or cached-as-authoritative alternatives"
substantively true; and extend Principle VI's named module list from four to
six. Complexity Tracking is empty — no violations.

Product version will move to 4.2.0 (MINOR: the shared surface gains, nothing
incompatible changes). Not bumped here — docs only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(008): task breakdown — 60 tasks across 9 phases

Phase 2 output. Tasks organised by user story so each ships and tests
independently; MVP is Phase 0-3 (US1).

Three things differ from what the task template would produce by default:

- Phase 0 is the constitution amendment, and it is the only phase that does
  not happen on this branch. Spec 006 recorded its amendment as already
  applied and explicitly not a task; ours is outstanding, so it becomes a
  blocking prerequisite on its own chore/ branch against main. Until it
  merges, an implementation that fetches a bundle page violates the very
  constraint that made the change reviewable.
- Tests are marked not-optional. The template treats them as opt-in, but the
  Development Workflow gate makes them mandatory here on three counts:
  verdict-adjacent logic, analysis-flow changes needing credential-free
  coverage, and the shared surface's headless property.
- FR-035 and FR-036 initially had no tasks — dropped because they are SHOULDs
  rather than MUSTs — and FR-019 was implemented but uncited. Added as
  T056/T057 and cited on T017. Coverage is now 46/46.

Validated mechanically: 60 tasks, IDs sequential T001-T060, every task
carrying a checkbox, id, optional labels and a concrete file path; all five
user stories represented; 18 tasks parallelisable.

The parallel win is Phase 2, where the erp.py and bundle.py chains share no
files and can run concurrently, then the four test modules together.

Each phase states its independent test criteria against a numbered quickstart
scenario. T060 singles out Scenario 2 to re-run after any report change: a row
reading "0 impacted" without its 11 undetermined devices is the failure mode
the design exists to prevent.

Docs only — VERSION untouched; its bump to 4.2.0 is T059.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(008): amend constitution to 3.2.0 — bundle page endpoint and scope-selector boundary

Enacted as a prerequisite of spec 008. Cisco publishes no API equivalent of a
bundle (a request for one returns HTTP 404), so a bundle's membership can only
be learned from its published page. That made two existing constraints block
the feature and left one principle's text stale.

Four changes:

- Networking admits sec.cloudapps.cisco.com, scoped to resolving a bundle to
  its advisory identifiers and its own identity. The request is explicitly
  unauthenticated and forbidden from carrying ANY inventory-derived data — a
  stricter bound than the advisory API's, which may carry the derived
  family/platform/release triple.
- A new Scope selectors bullet, rather than a relaxation of "No scraped or
  cached-as-authoritative alternatives". Reading a published page could be
  argued to survive that prohibition on a technicality, which is the kind of
  reasoning a governance document should foreclose. So the prohibition stands
  and a narrow enumerated permission sits beside it: a bundle page may supply
  which advisories are in scope and the bundle's own identity, and may not
  supply any advisory fact — title, severity, score, CVE, affected product,
  fixing release, or an advisory's publication date, every one of which the
  page actually displays. Disagreement resolves in the API's favour without
  exception, and a page yielding no advisory is an error rather than an empty
  scope, because an empty result rendered as a report reads as an all-clear.
  This narrows the tool's latitude while unblocking the feature.
- Principle VI's shared surface grows from four modules to six, adding erp
  (bundle resolution) and bundle (family derivation, filtering, row
  construction) — both fragile, safety-bearing logic a second consumer would
  otherwise reimplement, and a second divergent answer about which advisories
  are in scope is the harm the principle exists to prevent. bundle_report joins
  the terminal-side list beside html_report.
- Consumers now enumerates all six modules of the released interface.

A new constraint bullet plus materially expanded guidance, with no principle
removed or redefined and no existing permission widened: MINOR.

Applied on this feature branch rather than a standalone chore/ branch. Checking
the history corrected an earlier assumption: both prior feature-driven
amendments travelled in their feature's own PR — spec 006 in cb6938a (#9) and
spec 007 in e043d41 (#12). The one standalone constitution commit, 73de536
(#15), was retroactive, documenting Principle VI after 4.1.0 had shipped, so it
is not the precedent here. Keeping the amendment with the code it authorises
also keeps its justification in front of the reviewer.

Also realigns the spec-008 artifacts that asserted the amendment was still
outstanding: the plan's Constitution Check moves from CONDITIONAL PASS to PASS,
tasks Phase 0 is marked complete, and research Decision 9 records the applied
date and the rejected standalone-branch alternative. The README and
docs/index.html shared-surface updates remain tasks T051 and T052 — feature
work due in this same PR, not amendment debt.

Product VERSION untouched at 4.1.0; it moves to 4.2.0 as task T059.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(008): bundle-scoped impact reporting — one row per advisory (4.2.0)

Scope a run to a Cisco bundled publication by pasting its Event Response link,
and get back one row per advisory in that bundle rather than one row per
firewall. Each row carries the advisory as a link, its publication date, and
the products it impacts with the release that fixes them.

Adds no data source. The advisory id and publication date were already in the
payload retrieved per device, so filtering costs zero extra requests; only
advisories that impact nothing need a lookup (1 of 25 for the reference
bundle). Cisco publishes no bundle endpoint, so membership comes from the
published page — which contributes scope only, never an advisory fact.

New modules, taking the three roles the package already has:

  erp.py           I/O, like psirt: bundle page -> advisory identifiers
  bundle.py        pure, like matching: covered families, filtering, rows
  bundle_report.py rendering, like html_report: the per-advisory layout

erp and bundle join the shared surface, because the page parse and the family
derivation are the fragile parts a second consumer would otherwise reimplement.

The load-bearing property, and the reason for most of the design: a device
whose assessment reached no conclusion has NO advisory list at all, so it can
neither be asserted as impacted nor excluded as clean. Two mechanisms enforce
that rather than leaving it to review — the per-row accounting invariant
(impacted + unaffected + undetermined == population), which makes a dropped
device fail arithmetic instead of rendering as clean; and fail-open family
scoping, where a failed derivation widens to every family so its worst outcome
is a noisier report, never a suppressed device.

One bug found while verifying, worth calling out because it would have
misdirected patching. first_fix() returned one fixing release per row, while
FR-023 requires it per impacted product. Invisible on the FMC row, where both
affected releases share 7.4.4 — but cisco-sa-asaftd-ikev2-dos-eBueGdEG spans
13 releases needing 6 different fixes (7.0.9, 7.4.3, 7.7.11, 9.16.4.85,
9.18.4.71, 9.20.4.10). The old code would have shown one target for all 50
devices. Fixes are now per device, rendered per release group.

Verified twice end to end against ERP-75736 and the 70-row reference
inventory: 25 rows, zero accounting failures, identical row order across runs,
33 requests in ~75s (~3s over the whole-fleet baseline). Every number matches
the design's worked examples — FMC auth bypass 5/0/0 fixed in 7.4.4; ASA TCP
flood 0 impacted, 17 not impacted, 11 undetermined; FTD snort3 SSL 11/22/4;
IKEv2 50/0/15.

Tests: 272 -> 392, all without credentials or network, including the headless
property of both new shared-surface modules. Page fixtures are hand-authored
builders mirroring the verified live shape, following psirt_responses.py rather
than vendoring ~180 KB of Cisco's page.

Declining the prompt is byte-equivalent to today's behaviour, and no bundle
page is fetched on that path. --erp makes the capability scriptable, as
Principle III requires.

VERSION 4.1.0 -> 4.2.0 (MINOR: the shared surface gains erp and bundle;
nothing incompatible changes). Tag is pushed only after this PR merges.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(008): exercise the bundle flow in the container install test

The clean-room harness installed the new code but never touched the new
capability, so a --shell session would not have tested the thing it was spun up
to test.

- automated checks now assert --erp is advertised and requires a value
- the --shell banner walks both report shapes and the three failure paths, and
  shows the two filenames side by side so the layouts are easy to compare

Also records an ordering quirk that is confusing to hit unexplained: the
credential gate fires before bundle validation, so --erp with a bad value exits
1 (no credentials) rather than 2 (not a bundle address) until --config has run.
That order is deliberate — the tool refuses before making any request — so the
banner explains it instead of the behaviour changing.

Default mode installs the working tree, so this tests unreleased work without
pushing anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant