From b8b6a65f5b9709bd63378a1c1902ccc82b12aec1 Mon Sep 17 00:00:00 2001 From: Ali Bahaloo Date: Tue, 28 Jul 2026 12:53:27 -0700 Subject: [PATCH 1/7] =?UTF-8?q?chore:=20ignore=20uv.lock=20=E2=80=94=20no?= =?UTF-8?q?=20install=20path=20consumes=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index db55caa..fc719e1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,11 @@ venv/ /dist/ *.egg-info/ +# Byproduct of running `uv run`/`uv sync` in a checkout. No install path consumes it: +# the tool is installed with `uv tool install`, which resolves dependencies itself, and +# this package is also an importable library, so its dependencies stay loosely pinned. +/uv.lock + # Inputs / outputs inventory.xlsx impact_report_*.html From c44d6e9021e024a52ae1ada1c3a3993f68f2940b Mon Sep 17 00:00:00 2001 From: Ali Bahaloo Date: Tue, 28 Jul 2026 12:53:42 -0700 Subject: [PATCH 2/7] =?UTF-8?q?docs(008):=20BRD-005=20and=20spec=20?= =?UTF-8?q?=E2=80=94=20bundle-scoped=20impact=20reporting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .specify/feature.json | 2 +- brds/005-erp-bundle-scoped-report.md | 292 ++++++++++++++++++ .../checklists/requirements.md | 102 ++++++ specs/008-erp-bundle-scoped-report/spec.md | 255 +++++++++++++++ 4 files changed, 650 insertions(+), 1 deletion(-) create mode 100644 brds/005-erp-bundle-scoped-report.md create mode 100644 specs/008-erp-bundle-scoped-report/checklists/requirements.md create mode 100644 specs/008-erp-bundle-scoped-report/spec.md diff --git a/.specify/feature.json b/.specify/feature.json index ff19045..82acbd8 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1,3 +1,3 @@ { - "feature_directory": "specs/007-decommission-ai-analyzer" + "feature_directory": "specs/008-erp-bundle-scoped-report" } diff --git a/brds/005-erp-bundle-scoped-report.md b/brds/005-erp-bundle-scoped-report.md new file mode 100644 index 0000000..80151ba --- /dev/null +++ b/brds/005-erp-bundle-scoped-report.md @@ -0,0 +1,292 @@ +# Business Requirements Document (BRD): Bundle-Scoped Impact Reporting — Narrow the Assessment to a Cisco Event Response Bundle and Report per Advisory + +## Document Control + +| Field | Value | +| ----------------- | ------------------------------------------------------------ | +| BRD ID | BRD-005 | +| Title | Bundle-Scoped Impact Reporting (`caia` accepts a Cisco Event Response URL and reports per advisory) | +| Author | Ali Bahaloo / TELUS Digital | +| Status | Draft | +| Version | 0.1.0 | +| Created | 2026-07-28 | +| Last updated | 2026-07-28 | +| Related documents | Repo: https://github.com/xavient/caia-cli · BRD-003 (Inventory-Driven Vulnerability Check via the Cisco PSIRT API): `brds/003-api-inventory-check.md` · BRD-004 (Decommission the FueliX AI Analyzer): `brds/004-decommission-fuelix-ai-analyzer.md` · Constitution: `.specify/memory/constitution.md` · Spec 006: `specs/006-api-inventory-check/spec.md` · PSIRT access contract: `specs/006-api-inventory-check/contracts/psirt-access.md` · HTML report contract: `specs/006-api-inventory-check/contracts/html-report.md` · Cisco PSIRT openVuln API: https://developer.cisco.com/docs/psirt/ · Example bundle: https://sec.cloudapps.cisco.com/security/center/viewErp.x?alertId=ERP-75736 | + +## 1. Executive Summary + +Cisco publishes its Secure Firewall vulnerabilities in **bundled publications** — a single Event Response page listing every advisory released that day (ERP-75736, 4 March 2026, lists 25 advisories covering 48 CVEs across ASA, FMC and FTD). The ops team's actual job is bounded by one of those bundles: *"this bundle dropped — what in our fleet does it hit, and what do we patch?"* The tool today answers a broader question the other way round: one row per firewall, listing everything Cisco says about it. This BRD adds a bundle-scoped mode — the user pastes an Event Response URL, the tool narrows the assessment to the advisories in that bundle, and the HTML report inverts to **one row per advisory in the bundle**, showing the advisory identifier as a link, its publication date, and the products it impacts. Devices the tool could not conclusively assess are carried into every row they could belong to and marked as unknown, never quietly counted as clean. + +## 2. Business Context & Problem Statement + +The inventory-driven check delivered by BRD-003 is accurate and authoritative, but it is oriented along the wrong axis for the work the ops team actually does. + +- **The unit of work is an advisory, not a firewall.** A patch decision is made per advisory: it has a severity, a fix release, and a change window. The report today is one row per firewall with advisories nested underneath, so the thing an engineer must act on is the thing they have to reassemble by hand. +- **The bundle boundary is invisible.** Cisco's bundled publications are the trigger for the team's review cycle, and nothing in the tool knows a bundle exists. Measured against the current 70-row inventory, a full run reports **55 devices impacted by something**, spread across dozens of advisories published over several years. To answer *"what does ERP-75736 mean for us?"* someone must open the Event Response page, read 25 advisory titles, and then scan a 70-row report to work out which findings belong to the bundle and which are older noise. That reconciliation is manual, repeats on every bundle, and is exactly the kind of cross-referencing that silently drops a row. +- **The answer the team wants already exists in the data — it is just not rendered.** A trial run against ERP-75736 shows **24 of the 25 advisories touch at least one device**, with per-advisory impact ranging from 0 to 50 devices. That table is the deliverable. Producing it needs no new data source: the advisory identifier and publication date are already inside the payload the tool retrieves for every device today. +- **The undecided devices are the real hazard.** In the same run, **15 of 70 devices resolved to `Needs review`** — Cisco does not hold their release string, or does not list that release for their hardware. If a bundle-scoped report shows only positively-confirmed hits, those 15 devices vanish, and a row reading "3 impacted" is read as "the other 67 are fine." For 15 of them that is not a claim anyone made. Principle IV forbids it, and a per-advisory layout makes it much easier to commit than the per-device layout does. +- **The narrowing itself must be disclosed.** Filtering to a bundle deliberately suppresses findings — advisories that genuinely affect the fleet but were published in some other bundle. A report that hides that would read as a full fleet assessment while being a partial one. That is a new failure mode this feature creates, and it has to be designed out rather than discovered later. + +The cost of inaction is a review cycle where the authoritative data is present but the team still assembles the actual answer in a spreadsheet, once per bundle, with no record of what was left out. + +## 3. Business Objectives & Goals + +- **G1** — Let a user scope a run to one Cisco bundled publication by pasting its Event Response URL, with no manual transcription of advisory identifiers. +- **G2** — Deliver the report along the axis the work happens on: one row per advisory in the bundle, showing which products it impacts. +- **G3** — Make an advisory in the bundle that impacts nothing an explicit, visible result, so a clean outcome is evidence rather than an absence. +- **G4** — Guarantee that a device the tool could not conclusively assess is never rendered, counted, or summarised as unaffected in any bundle row. +- **G5** — Make the report state plainly what the bundle filter suppressed, so it can never be mistaken for a whole-fleet assessment. +- **G6** — Keep every displayed advisory fact sourced from Cisco's API, using the Event Response page only to learn *which advisories are in scope*. +- **G7** — Preserve today's behaviour for a user who does not supply a bundle: same verdicts, same whole-fleet report, same exit statuses. +- **G8** — Keep the mode usable unattended, so a bundle review can be scripted or scheduled rather than typed. + +## 4. Stakeholders & Users + +| Stakeholder / user | Role in this product | What they need from it | +| ------------------ | -------------------- | ---------------------- | +| Security / network analyst | Primary user | To paste the bundle link Cisco emailed them and get back the per-advisory impact table, without transcribing 25 identifiers | +| Firewall / patching engineer | Downstream actor | Per advisory: which devices, and the release that fixes it — the worklist for a change window | +| Team lead / security manager | Reviewer, reporting | A defensible per-advisory record of the bundle review, including what was undecided and what was out of scope | +| Inventory owner / CMDB steward | Indirectly accountable | To see which inventory rows caused an undecided result, and what to correct so the next bundle review is conclusive | +| IT / security review | Reviewer | Confidence that verdicts still come only from Cisco's API, that a scraped page cannot introduce a fact, and that no device identity leaves the machine | +| Maintainers | Owners | One assessment engine feeding two report shapes, with the verdict rules unchanged and untouched | +| Automation / scheduled job | Non-interactive consumer | A way to run a bundle review without a prompt | + +## 5. Scope + +### 5.1 In Scope + +- Prompting for a Cisco Event Response URL as part of a normal run, after the inventory is located. +- Accepting the same URL non-interactively so a bundle review can be scripted. +- Resolving an Event Response URL to the set of advisory identifiers it publishes. +- Narrowing the completed assessment to the advisories in that bundle. +- A bundle-scoped HTML report laid out one row per advisory in the bundle, carrying: the advisory identifier as a link that opens in a new tab, the advisory's publication date, and the impacted products. +- Rendering products whose status for a given advisory could not be determined as visually distinct (yellow) and explicitly labelled as undetermined. +- Rendering advisories in the bundle that impact nothing as present-and-clean rows. +- Disclosing, in the report, how many findings the bundle filter suppressed. +- Retaining the current whole-fleet report for runs where no bundle is supplied. +- Failure handling for an unreachable, unrecognised, or empty Event Response page. + +### 5.2 Out of Scope + +- **Changing any verdict rule.** Release normalisation, model-to-platform mapping, and the `Impacted` / `Not impacted` / `Needs review` decision table are untouched. This feature filters and re-renders an assessment; it does not produce one. +- **Treating the Event Response page as a source of advisory facts.** Titles, severities, CVEs, scores and dates come from Cisco's API even when the page also shows them. The page contributes a list of identifiers and nothing else. +- **More than one bundle per run.** One bundle, one report. +- **Arbitrary Cisco URLs.** Individual advisory pages, security bulletins, and non-Cisco vulnerability feeds are not accepted inputs. A single-advisory mode is a plausible successor feature, not this one. +- **CVE-level rows.** Rows are per advisory; an advisory's CVEs are shown within its row. A bundle where several advisories share a CVE is not decomposed. +- **Remediation tracking.** No state is carried between runs — no "patched since last bundle", no ticket integration, no history. Each report is a point-in-time record. +- **Caching the bundle offline.** The Event Response page is fetched per run. Snapshotting bundles for air-gapped use is not included. +- **Removing or restyling the existing whole-fleet report.** It remains the output when no bundle is supplied. Whether the bundle report eventually replaces it is an open question (§13), not a commitment here. +- **Inferring which advisories *should* have been in a bundle.** The bundle's membership is whatever Cisco publishes on that page. + +## 6. User Journeys *(feeds the spec's prioritized user stories)* + +### Journey 1 — Review a bundled publication against the fleet (Priority: P1) + +- **Actor:** Security / network analyst +- **Trigger:** Cisco publishes a bundled publication; the analyst has the Event Response link. +- **Outcome / value:** One HTML report, one row per advisory in that bundle, showing exactly which products each advisory impacts and which could not be determined — the whole bundle review in a single artifact, with no manual cross-referencing. +- **Flow:** + 1. The analyst runs the tool in the folder holding the inventory. + 2. The tool locates the inventory as it does today, and prompts for an Event Response URL. + 3. The analyst pastes the URL. + 4. The tool confirms what it found — the bundle's identifier, title, and the number of advisories it lists — before doing any work against the inventory. + 5. The tool assesses the inventory against Cisco's data as it does today, reporting progress. + 6. The tool narrows the result to the bundle's advisories and writes the report. + 7. The analyst opens the report: one row per bundle advisory, ordered deterministically, each row showing the advisory identifier as a link, its publication date, the products impacted, and the products whose status is undetermined. +- **Acceptance:** + - **Given** a valid Event Response URL listing 25 advisories and an inventory of 70 devices, **When** the run completes, **Then** the report contains exactly 25 advisory rows — one per advisory in the bundle, including any that impact nothing. + - **Given** an advisory in the bundle that Cisco lists against 5 inventory devices, **When** the analyst reads that row, **Then** all 5 devices are named, and the release that first fixes the advisory is shown for each. + - **Given** an advisory row, **When** the analyst clicks the advisory identifier, **Then** Cisco's published advisory opens in a new tab. + - **Given** any advisory row, **When** the analyst reads it, **Then** it shows the advisory's publication date as published by Cisco. + - **Given** a device the tool could not conclusively assess, **When** it appears in an advisory row, **Then** it is visually distinct (yellow) and labelled as undetermined, and it is not included in any count of impacted or unaffected products. + - **Given** the assessment found advisories affecting the fleet that are not in the bundle, **When** the analyst reads the report, **Then** the report states how many such findings were suppressed by the bundle filter. + - **Given** an advisory in the bundle that impacts no inventory device, **When** the analyst reads its row, **Then** the row is present and states that nothing in the inventory is impacted, alongside the count of devices whose status was undetermined. + +### Journey 2 — Undecided devices are actionable, not just flagged (Priority: P2) + +- **Actor:** Inventory owner / CMDB steward +- **Trigger:** A bundle report shows devices with undetermined status. +- **Outcome / value:** A named list of those devices with the reason each could not be assessed, so the underlying inventory data can be corrected and the next bundle review comes back conclusive. +- **Flow:** + 1. The steward opens the bundle report. + 2. A dedicated section lists every device whose assessment was undetermined, exactly once. + 3. Each entry gives the device, its recorded hardware and release, and the reason — for example that Cisco does not hold that release string, or does not list it for that hardware. + 4. The steward corrects the inventory and the next run resolves those devices. +- **Acceptance:** + - **Given** 15 devices with an undetermined assessment, **When** the steward opens the report, **Then** all 15 are listed once each with a human-readable reason. + - **Given** a device whose recorded release Cisco does not list for its hardware, **When** the steward reads its entry, **Then** the entry shows both what the inventory recorded and what Cisco was asked, so the field to correct is evident. + +### Journey 3 — Run a bundle review unattended (Priority: P2) + +- **Actor:** Automation / scheduled job, or an analyst reviewing several bundles in succession +- **Trigger:** A scheduled review, or a batch of bundles to work through. +- **Outcome / value:** The same bundle-scoped report with no interactive prompt, so the review can be scripted. +- **Flow:** + 1. The job invokes the tool with the Event Response URL supplied up front. + 2. The tool performs the same run as Journey 1 without prompting. + 3. The report is written and the exit status reflects the outcome. +- **Acceptance:** + - **Given** the URL is supplied up front, **When** the tool runs with no terminal attached, **Then** it never prompts and writes the same report Journey 1 produces. + - **Given** the supplied URL cannot be resolved to a bundle, **When** the tool runs unattended, **Then** it fails with a distinct, non-zero exit status and writes no report. + +### Journey 4 — Skip the bundle and get today's report (Priority: P2) + +- **Actor:** Existing user +- **Trigger:** A run where the user wants the whole-fleet view, or has no bundle to hand. +- **Outcome / value:** Existing behaviour, unchanged — no new obligation imposed on users who do not need bundle scoping. +- **Flow:** + 1. The user runs the tool. + 2. At the Event Response prompt, the user declines. + 3. The tool produces today's whole-fleet report. +- **Acceptance:** + - **Given** the user declines at the prompt, **When** the run completes, **Then** the report is the current whole-fleet report, with the same verdicts and reasons as before this feature. + - **Given** no bundle is supplied non-interactively, **When** the tool runs, **Then** it behaves exactly as it does today. + +### Journey 5 — Recover from a bad bundle link (Priority: P3) + +- **Actor:** Security / network analyst +- **Trigger:** A mistyped, stale, or non-bundle URL, or Cisco's site being unreachable. +- **Outcome / value:** A clear statement of what went wrong and what to do, without losing the run or producing a misleading report. +- **Flow:** + 1. The analyst pastes a URL that cannot be resolved to a bundle. + 2. The tool says which of the distinguishable failures occurred — not a Cisco URL, page unreachable, page reachable but no advisories found — and re-prompts. + 3. The analyst corrects the URL, or declines and gets the whole-fleet report. +- **Acceptance:** + - **Given** a URL that is not a Cisco Event Response address, **When** it is submitted, **Then** the tool rejects it before making any network request and re-prompts. + - **Given** Cisco's site cannot be reached, **When** the tool tries to resolve the bundle, **Then** it says the failure is connectivity and not a bad link, and no partial or empty bundle report is written. + - **Given** a reachable page from which no advisory identifiers can be extracted, **When** the tool reports it, **Then** it says so explicitly rather than proceeding with an empty bundle, and writes no report. + +## 7. Business Requirements + +| ID | Requirement | Priority | +| ----- | ------------------------------------ | -------- | +| **Bundle input** | | | +| BR-01 | The product MUST prompt for a Cisco Event Response URL during a normal run, after the inventory is located and before any request about the inventory is made. | P1 | +| BR-02 | The product MUST accept the same URL supplied up front so a bundle review needs no prompt, and MUST NOT prompt at all when it is supplied that way. | P2 | +| BR-03 | The product MUST allow the user to decline the prompt, and MUST then produce the current whole-fleet report with behaviour identical to today. | P2 | +| BR-04 | The product MUST validate that a submitted URL is a Cisco Event Response address before making any network request, and MUST reject anything else with a message naming the expected form. | P1 | +| BR-05 | The product MUST accept the URL in the form Cisco publishes it, and SHOULD additionally accept a bare bundle identifier (e.g. `ERP-75736`). | P3 | +| **Resolving the bundle** | | | +| BR-06 | The product MUST resolve an Event Response URL to the complete set of advisory identifiers that bundle publishes. | P1 | +| BR-07 | The product MUST use the Event Response page **only** to determine which advisories are in scope. It MUST NOT take any advisory fact — title, severity, score, CVE, date, fix release, or affected product — from that page. | P1 | +| BR-08 | The product MUST source every advisory fact it displays from Cisco's advisory API, including for advisories in the bundle that impact no inventory device. | P1 | +| BR-09 | The product MUST confirm what it resolved — bundle identifier, bundle title, and advisory count — before beginning work against the inventory, so a wrong link is caught before the run is spent. | P2 | +| BR-10 | The product MUST distinguish, and report differently, these outcomes: the URL is not a bundle address; Cisco could not be reached; the page was reached but yielded no advisory identifiers. | P1 | +| BR-11 | The product MUST NOT write a bundle report when the bundle could not be resolved, and MUST exit with a non-zero status that a script can distinguish from a successful run. | P1 | +| BR-12 | The product MUST treat everything retrieved from the Event Response page as untrusted input that cannot alter the report's structure, styling, or behaviour, and cannot become executable content. | P1 | +| **Filtering** | | | +| BR-13 | The product MUST assess the inventory using the existing rules, then narrow the result to the bundle's advisories. The narrowing MUST NOT change any device's verdict. | P1 | +| BR-14 | The product MUST report how many distinct advisories affecting the inventory were suppressed by the bundle filter, so the report cannot be mistaken for a whole-fleet assessment. | P1 | +| BR-15 | The product MUST produce a row for every advisory in the bundle, including advisories that impact no inventory device. | P1 | +| **Report layout** | | | +| BR-16 | The report MUST be laid out one row per advisory in the bundle. | P1 | +| BR-17 | Each row MUST show the advisory identifier as a link to Cisco's published advisory that opens in a new tab without granting the opened page access to the report. | P1 | +| BR-18 | Each row MUST show the advisory's publication date as published by Cisco. | P1 | +| BR-19 | Each row MUST name every inventory product Cisco lists as impacted by that advisory. | P1 | +| BR-20 | Each row MUST show, for each impacted product, the release Cisco lists as first fixing that advisory; a fix release Cisco does not supply MUST be shown as absent, never inferred. | P1 | +| BR-21 | Each row MUST render products whose status for that advisory could not be determined in a visually distinct way (yellow) and MUST label them as undetermined rather than leaving colour to carry the meaning alone. | P1 | +| BR-22 | Each row MUST account for every in-scope device exactly once across impacted, confirmed unaffected, and undetermined, so the row's numbers can be checked against the inventory size. | P1 | +| BR-23 | An undetermined device MUST appear in every bundle row whose advisory could apply to it, and MUST NOT be omitted from a row merely because its status there is unknown. Where the advisory's applicability to that device cannot be established, the product MUST include it rather than omit it. | P1 | +| BR-24 | The report MUST NOT present, count, or summarise an undetermined device as unaffected anywhere, including in totals, headline figures, and per-row counts. | P1 | +| BR-25 | The report MUST order advisory rows deterministically, so two runs over the same inputs produce the same document order. | P1 | +| BR-26 | The report MUST list every undetermined device once, with the hardware and release the inventory recorded, what was asked of Cisco, and a human-readable reason. | P2 | +| BR-27 | The report MUST identify the bundle it covers — identifier, title, and Cisco's publication date — and link to the Event Response page. | P2 | +| BR-28 | The report MUST remain a single self-contained file that renders with no network access, and MUST NOT overwrite an existing report. | P1 | +| BR-29 | The report SHOULD let a reader filter and sort the advisory rows, at least by severity and by number of impacted products. | P3 | +| BR-30 | The report SHOULD provide a per-device cross-reference so a reader can go from a device to every bundle advisory affecting it. | P3 | +| **Cross-cutting** | | | +| BR-31 | Resolving a bundle MUST NOT transmit any inventory-derived data. Nothing about any device may be sent to the Event Response page. | P1 | +| BR-32 | A run that stops early MUST keep every answer already obtained, report that it was partial and why, and leave unreached devices undetermined — matching today's behaviour. | P1 | +| BR-33 | The capability MUST be usable without a terminal: bundle resolution and filtering MUST NOT prompt, print, or exit the process. Prompting and report writing remain terminal concerns. | P1 | +| BR-34 | The added network cost of a bundle-scoped run MUST stay within Cisco's published quotas, and SHOULD add no more requests than the number of bundle advisories that impact nothing. | P2 | + +## 8. Success Metrics & Measurable Outcomes + +- **SC-01** — An analyst can go from a pasted Event Response URL to a complete per-advisory impact report in one command, with no manual transcription of advisory identifiers and no cross-referencing by hand. +- **SC-02** — For a bundle of 25 advisories, the report contains 25 advisory rows; the count matches the bundle page exactly, with no advisory missing and none invented. +- **SC-03** — Every advisory row's product counts sum to the number of in-scope devices, so the report is internally checkable against the inventory. +- **SC-04** — No device with an undetermined assessment appears as unaffected anywhere in the report; each is reachable from every row it could belong to and is listed once with a reason. +- **SC-05** — Every advisory fact rendered in the report is present in Cisco's API response for that advisory; none originates from the Event Response page. +- **SC-06** — A user who declines the bundle prompt gets a report byte-for-byte equivalent in content to the one produced before this feature. +- **SC-07** — Each of the three bundle-resolution failures — not a bundle address, unreachable, no advisories found — produces a distinct message, and none produces a report. +- **SC-08** — A bundle review of a 70-device inventory completes within roughly the time a whole-fleet run takes today, and stays within Cisco's per-minute and per-day quotas. +- **SC-09** — The report states the number of suppressed out-of-bundle findings, and a reader can tell from the report alone that it is bundle-scoped rather than whole-fleet. + +## 9. Assumptions + +Each of these was verified against the live sources on 2026-07-28 using ERP-75736 and the current 70-row inventory, rather than assumed from documentation. + +- **The bundle's advisory list is machine-readable from the Event Response page.** The page returns HTTP 200 in under a second and yields exactly its 25 advisory identifiers by extracting them from the advisory links in its table. The identifiers are **not** present in the page's visible text — only in the links — so text extraction alone is insufficient. +- **The advisory identifier and publication date are already in the data the tool retrieves.** Every advisory object returned for a device carries `advisoryId`, `advisoryTitle`, `sir`, `cvssBaseScore`, `cves`, `publicationUrl`, `firstPublished`, and the per-platform first-fix release. Filtering an assessment to a bundle therefore costs no additional requests, and the publication date needs no separate lookup for any advisory that impacts at least one device. +- **Advisories in the bundle that impact nothing need their own lookup.** Such an advisory never appears in any device's result, so its title and publication date must be fetched individually. In ERP-75736 that is 1 advisory of 25; the worst case is the bundle size. +- **There is no bundle endpoint in Cisco's API.** A request for the bundle by identifier returns HTTP 404. Resolving a bundle to its advisories requires reading the published page. A date-window query over Cisco's API returns a superset of a bundle's advisories (27 for ERP-75736's publication date, including 2 unrelated same-day publications), which is a viable cross-check but not a substitute for the bundle's actual membership. +- **A device with an undetermined assessment has no advisory list at all.** Cisco was never able to answer for it, so its status against any specific advisory is genuinely unknown — it cannot be placed in an advisory row as a fact, and it cannot be excluded as clean. This is the structural reason BR-21 through BR-24 exist. +- **Bundle-scoped runs assess the whole inventory.** Narrowing the assessment itself to the families a bundle names would be an optimisation, but it depends on interpreting Cisco's product-name strings, and a family named only in an advisory's prose would be missed. The safe default is to assess the whole inventory and filter afterwards; a whole-inventory run over 70 devices costs 32 requests and about 72 seconds, so the optimisation buys little. +- **Bundle membership is stable enough to fetch per run.** Event Response pages are versioned and revised occasionally; fetching per run means a revision is picked up without a tool release. +- **Products in a row are identified the way the inventory names them**, so a reader can match a row back to their own records. + +## 10. Constraints + +- **Constitution IV (Conservative & Traceable Analysis, non-negotiable).** Every verdict must remain traceable to Cisco's published API data. The bundle page may contribute scope only — never a fact, and never a verdict. An undetermined device must never be presented as unaffected. This constrains the report layout directly and is the source of BR-07, BR-08, and BR-21 through BR-24. +- **Constitution: "No scraped or cached-as-authoritative alternatives."** Reading the Event Response page is permitted here only because it supplies a *set of identifiers to filter by*, and every fact displayed is independently retrieved from the API. The spec must make that boundary explicit and testable, not merely intended. +- **Constitution: Networking.** The analyzer today contacts only `apix.cisco.com` and `id.cisco.com`. Fetching an Event Response page adds `sec.cloudapps.cisco.com` as an endpoint the analyzer itself contacts, which the constitution designates a reviewed change. **This BRD requires a constitution amendment before implementation** — the same gate BRD-003 passed through for the API hosts. +- **Constitution V (Secrets Hygiene & Data Locality).** Bundle resolution must send no inventory-derived data. The existing bound — only family, platform, and release may leave the machine — is unchanged and must not be widened. +- **Constitution VI (Headless Shared Surface).** Bundle resolution and filtering must be callable without a terminal: no printing, prompting, working-folder access, file writing, or process exit. The prompt, the confirmation, and the report belong to the terminal layer. A failure to *reach* Cisco must surface as an error, not as a set of undecided verdicts. +- **Constitution I (Standard-Library-First).** No new runtime dependency for fetching or parsing the Event Response page. +- **Constitution II (Cross-Platform Parity)** and **III (CLI-First, Scriptable)**. Identical behaviour on macOS, Windows, and Linux; and because the prompt cannot be the only route in, the non-interactive path (BR-02) is a constitutional requirement rather than a convenience. +- **Report constraints carry over.** Single self-contained file, renders with no network access, never overwritten, and all Cisco-supplied content treated as untrusted text. Content from the Event Response page inherits the same treatment. +- **Cisco's quotas** — 5 requests/second, 30/minute, 5,000/day — and the existing pacing continue to apply. +- **Coverage inherited from Cisco.** The API holds ASA, FMC, FTD and FXOS data only from January 2022 onward. A bundle predating that window cannot be answered, and the report must not present the resulting silence as a clean result. + +## 11. Dependencies + +- **Cisco PSIRT openVuln API** (`apix.cisco.com`, `id.cisco.com`) — input; the sole source of every advisory fact and every verdict. Unchanged by this feature. +- **Cisco Security Center Event Response pages** (`sec.cloudapps.cisco.com`) — input; supplies the set of advisory identifiers in a bundle. New endpoint; see §10. +- **The existing assessment capability** (BRD-003 / spec 006) — input; produces the per-device result this feature filters. Must be reused, not reimplemented. +- **The existing HTML report capability** — this feature adds a second layout alongside it; both must keep the self-containment and escaping guarantees. +- **The user's inventory workbook** — input, unchanged in format. +- **Cisco PSIRT API credentials** — input, unchanged; the tool must still refuse to run without them. +- **The project constitution** — must be amended for the new endpoint before implementation; output of that amendment gates this work. + +## 12. Risks & Mitigations + +| Risk | Impact | Likelihood | Mitigation | +| ------ | ------ | ---------- | ---------- | +| An undetermined device is read as unaffected in a per-advisory row — the layout invites it far more than the per-device layout does | H | M | BR-21 through BR-24: undetermined devices carried into every row they could belong to, labelled in words as well as colour, excluded from every count of impacted or unaffected, and each row's numbers made to sum to the in-scope device count so an omission is visible | +| A bundle-scoped report is mistaken for a whole-fleet assessment, and out-of-bundle exposure goes unpatched | H | M | BR-14 and BR-27: the report names the bundle it covers and states how many findings the filter suppressed | +| Cisco changes the Event Response page's markup and bundle resolution silently returns fewer advisories, or none | H | M | BR-09 confirms the resolved advisory count to the user before the run; BR-10 and BR-11 make an empty result an explicit failure that writes no report, never a silently empty bundle | +| Colour alone conveys the undetermined state, failing print, greyscale, and colour-blind readers | M | H | BR-21 requires an explicit label alongside the colour | +| An advisory in the bundle affects a product family absent from the inventory, and its zero-impact row is read as reassurance about products the tool never assessed | M | M | BR-22's per-row accounting makes the in-scope device population explicit for every row | +| A bundle predates Cisco's data coverage, so nothing can be answered and silence looks clean | M | L | §10 records the coverage boundary; the spec must require it to be surfaced rather than absorbed | +| The added lookups for zero-impact advisories push a large bundle toward the quota | L | L | BR-34 bounds the additional requests at the bundle's advisory count; measured worst case is far inside the daily quota | +| The bundle page is reachable but slow, stalling a run before any useful work | L | M | Bundle resolution happens before the inventory work (BR-01) and must be time-bounded, so a stall fails fast and cheaply | +| Two report layouts diverge in their treatment of undetermined devices | M | M | BR-33 keeps one assessment feeding both layouts; the verdict rules stay in one place per Constitution VI | + +## 13. Open Questions + +- **What is the counting unit for "impacted products"?** A trial run shows `CVE-2026-20079` impacting **5 devices**, which sit on **2 distinct affected releases** and **3 model/release combinations**. The originating request described this case as "2 products", suggesting distinct release rather than device is the intended unit. The report should probably lead with the device count and roll up by release beneath it — but this needs settling explicitly, because it changes every number in the report. +- **Does the bundle report eventually replace the whole-fleet report, or stand alongside it permanently?** BR-03 keeps both for now. If it replaces it, that is a breaking change to the product's primary output and should drive a MAJOR version bump; if it stands alongside, the shared surface only gains and a MINOR bump suffices. +- **How should advisory rows be ordered by default?** Severity first, then impacted count, is the obvious candidate; bundle-page order is the alternative and is arguably more auditable against Cisco's own publication. BR-25 requires only that the order be deterministic. +- **Should a bundle row distinguish "no devices of this family in the inventory" from "devices of this family were checked and are clean"?** Both currently render as zero impacted, and they mean very different things to a reader. +- **Should the date-window cross-check be used?** A date-window query over Cisco's API returns a superset of a bundle's advisories and could flag a page-parsing regression by disagreeing with the scraped list. Worth one request, or unnecessary complexity? +- **Should a resolved bundle be recorded in the report as raw evidence** — the identifier list as resolved — so a reader can audit the scope the report claims? +- **Is a single-advisory mode wanted next?** The same mechanism answers "does this one advisory affect us?" for a single identifier or CVE. Out of scope here, but it is the natural successor and may influence how the input is modelled. + +## 14. Glossary + +| Term | Definition | +| ------ | ------------ | +| Advisory | A single Cisco Security Advisory, identified by a `cisco-sa-…` identifier, covering one or more CVEs. | +| Bundled publication | Cisco's practice of releasing many Secure Firewall advisories on one date, announced together on a single page. | +| Event Response (ERP) page | The Cisco Security Center page announcing a bundled publication and listing the advisories it contains. Identified as `ERP-#####`. | +| Bundle | Within this document, the set of advisories one Event Response page publishes. The unit of scope this feature introduces. | +| Bundle-scoped report | The new report layout: one row per advisory in the bundle. | +| Whole-fleet report | The existing report layout: one row per inventory device. | +| Impacted | Cisco positively listed the advisory as applying to that device's software release on its hardware platform. | +| Not impacted / confirmed unaffected | Cisco confirmed both that it recognises the release and that the release runs on that hardware, and returned no advisory. Only ever a positive confirmation. | +| Undetermined / Needs review | The tool could not conclusively assess the device — the release is unknown to Cisco, is not listed for that hardware, a required field is missing, or the request could not be completed. Never presented as unaffected. | +| First fix | The release Cisco lists as first resolving a given advisory on a given platform. The field an engineer acts on. | +| In-scope device | An inventory device belonging to a product family the tool can query, and therefore counted in a bundle row's accounting. | +| Suppressed finding | An advisory affecting the inventory that is not in the reviewed bundle, and is therefore absent from the bundle-scoped report. | +| Shared surface | The modules that produce a verdict and must remain callable without a terminal (Constitution VI). | diff --git a/specs/008-erp-bundle-scoped-report/checklists/requirements.md b/specs/008-erp-bundle-scoped-report/checklists/requirements.md new file mode 100644 index 0000000..d8d6477 --- /dev/null +++ b/specs/008-erp-bundle-scoped-report/checklists/requirements.md @@ -0,0 +1,102 @@ +# Specification Quality Checklist: Bundle-Scoped Impact Reporting + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-07-28 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Items marked incomplete require spec updates before `/speckit-clarify` or `/speckit-plan` + +### Iteration 1 — 2026-07-28 + +Two [NEEDS CLARIFICATION] markers remain, both raised deliberately rather than guessed. Each +changes numbers a reader acts on, and each has more than one defensible reading: + +1. **FR-022 — the counting unit for "impacted products."** Device, distinct affected release, or + hardware-model-and-release combination. Against the current inventory `CVE-2026-20079` yields + 5, 2, and 3 respectively. The originating request described the expected answer as "2 + products", which points away from the per-device reading a naive implementation would take — + so this cannot be defaulted safely. +2. **FR-025 — the evaluated population per row.** Every queryable inventory device, or only + devices in the families an advisory names. This sets the denominator behind FR-025 and FR-026 + and interacts with FR-027's undetermined-device scoping. + +Both are scope-level decisions under the prioritisation rule (scope > security/privacy > UX > +technical), which is why they were kept as markers rather than resolved by assumption. + +Fixes applied in this iteration: + +- **Scope bounding.** Eight explicit scope boundaries added to Assumptions, following the house + convention in specs 006 and 007 of stating boundaries inline rather than in a separate section. +- **SC-008 made measurable.** "Within roughly the time a whole-fleet run takes today" replaced + with a bounded comparison (no more than 25% added elapsed time), so it can be verified. +- **FR-022 strengthened** to require the count to be stated, not merely the products listed. + +### Iteration 2 — 2026-07-28 + +Both clarifications answered and folded in; **all 16 items now pass**. Recorded in the spec's +Clarifications section with the reasoning, and propagated to every dependent requirement rather +than only to the two that carried the markers: + +- **Counting unit = device**, with a per-row rollup by affected release (FR-022, FR-022a). The + decisive constraint was FR-025: a release-based denominator cannot close, because an + undetermined device often has no release Cisco recognises and could not be placed in one. +- **Row population = family-scoped**, whole-run totals stated once (FR-025, FR-026, FR-026a). + A single whole-inventory denominator was rejected on safety grounds, not readability: ~375 + undetermined markers across 25 rows, most of them meaningless, would train readers to + disregard the marker Principle IV depends on. +- **FR-027a/FR-027b added** to pin down family derivation — union of the families Cisco returned + the advisory for and those in its published product list, failing open to every family. FR-027b + makes the direction of the safety guarantee explicit: scoping may only narrow which rows an + *undetermined* device appears in, never suppress an impacted device or manufacture an + unaffected one. This replaced what had been deferred to planning, since leaving it open would + have let the plan choose an unsafe narrowing. +- **Dependent sections realigned**: two new acceptance scenarios for family scoping and one for + the fail-open case, a new Covered family set entity, FR-026a's run totals reflected in SC-003, + and the row-population item removed from Deferred to planning. +- **One item newly deferred**: how a row with many impacted devices is presented without + truncation. One advisory in the reference bundle impacts 50 of 70 devices, and FR-033's + self-containment rules out loading anything on demand. + +### Constitutional notes carried to planning + +Not checklist items, but recorded here so the plan's Constitution Check cannot miss them: + +- Fetching a bundle page adds `sec.cloudapps.cisco.com` as an endpoint the analyzer itself + contacts. Technology & Data Constraints designate that a reviewed change, requiring a + constitution amendment **before implementation** — the same gate spec 006 passed through for + the advisory API hosts. Precedent for the mechanism: + `chore/constitution-headless-shared-surface` → constitution 3.1.0. +- Principle IV is the binding constraint on the report layout: FR-024 through FR-028 exist + because a device whose assessment is undetermined has no advisory list at all, so it can + neither be asserted as impacted nor excluded as clean. +- Principle VI requires bundle resolution and filtering to be headless (FR-039, FR-040); the + prompt and report writing stay in the terminal program. +- Principle V bounds what may leave the machine: bundle resolution transmits no inventory-derived + data (FR-037). diff --git a/specs/008-erp-bundle-scoped-report/spec.md b/specs/008-erp-bundle-scoped-report/spec.md new file mode 100644 index 0000000..f9512f0 --- /dev/null +++ b/specs/008-erp-bundle-scoped-report/spec.md @@ -0,0 +1,255 @@ +# Feature Specification: Bundle-Scoped Impact Reporting + +**Feature Branch**: `008-erp-bundle-scoped-report` + +**Created**: 2026-07-28 + +**Status**: Draft + +**Input**: User description: "BRD-005 — Bundle-Scoped Impact Reporting: narrow the assessment to a Cisco Event Response bundle and report per advisory (`brds/005-erp-bundle-scoped-report.md`)" + +## Clarifications + +### Session 2026-07-28 + +- **Q: What is one "impacted product" — a device, a distinct affected release, or a hardware-and-release combination?** → **The device is the unit of count**, with a per-row rollup by affected release alongside it (FR-022, FR-022a). Decisive reason: FR-025's per-row accounting only closes against a device population. Releases cannot sum to one, because an undetermined device frequently has no release Cisco recognises and so could not be placed in a release-based denominator at all. Counting devices also keeps the bundle report commensurable with the whole-fleet report and the inventory itself, which are per device throughout. The release rollup preserves the "how many upgrade decisions" reading — e.g. *5 devices → 2 affected releases → all fixed by 7.4.4* — without making it the denominator. + +- **Q: What population does each advisory row report against?** → **Per-row, scoped to the product families that advisory covers** (FR-025, FR-026), with the whole-run totals stated once separately (FR-026a). A single whole-inventory denominator was rejected: with 15 undetermined devices across 25 rows it produces roughly 375 undetermined markers, most of them meaningless — an ASA appliance flagged undetermined against an FMC-only advisory it cannot run — which trains a reader to disregard the very marker Principle IV depends on. Family scoping was found to need far less inference than first assumed: for any advisory Cisco returned for at least one device, the families follow from which family-scoped question returned it, and for an advisory impacting nothing the families come from its own published product list, which was verified to resolve cleanly. FR-027a unions both sources and fails open to every family, so scoping can only ever reduce noise, never suppress a device (FR-027b). + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Review a bundled publication against the fleet (Priority: P1) + +Cisco publishes its Secure Firewall vulnerabilities in bundled publications — one Event Response page listing every advisory released that day. An analyst runs the tool in the folder holding the inventory, is prompted for the bundle's URL, and pastes it. The tool reports which bundle it found and how many advisories it lists, assesses the inventory against Cisco's published data exactly as it does today, then writes an HTML report laid out **one row per advisory in the bundle** — each row naming the advisory, linking to it, giving its publication date, and listing the products it impacts along with the release that fixes each. Products the tool could not conclusively assess appear in the row too, visibly distinct and labelled as undetermined. + +**Why this priority**: This is the feature. It converts an assessment the tool already produces into the shape the ops team's review cycle actually runs on, and it eliminates the manual cross-referencing that stands between a published bundle and a patch decision today. Every other story in this spec supports, hardens, or cleans up after it. + +**Independent Test**: Run the tool in a folder containing a valid inventory, paste a real Event Response URL at the prompt, and confirm the resulting report contains one row per advisory the bundle lists, each carrying an advisory link, a publication date, the impacted products with their fixing releases, and any undetermined products marked as such. Delivers the whole value of the change on its own. + +**Acceptance Scenarios**: + +1. **Given** a valid Event Response URL listing 25 advisories and an inventory of 70 devices, **When** the run completes, **Then** the report contains exactly 25 advisory rows — one per advisory the bundle lists — including any that impact nothing. +2. **Given** an advisory the bundle lists that Cisco records as applying to 5 inventory devices across 2 affected releases, **When** the analyst reads that row, **Then** all 5 devices are named, the count of 5 devices and the rollup to 2 affected releases are both stated, and each device shows the release Cisco lists as first fixing that advisory. +3. **Given** an advisory row, **When** the analyst activates the advisory identifier, **Then** Cisco's published advisory opens in a new tab and the opened page has no access to the report. +4. **Given** any advisory row, **When** the analyst reads it, **Then** it shows that advisory's publication date exactly as Cisco publishes it. +5. **Given** a device whose assessment was undetermined, **When** it appears in an advisory row, **Then** it is visually distinct from impacted products, is labelled as undetermined in words as well as by colour, and is counted in neither the impacted nor the unaffected total. +6. **Given** an advisory the bundle lists that impacts no inventory device, **When** the analyst reads its row, **Then** the row is present, states that no inventory device is impacted, names the product families and population size it was evaluated against, and still shows how many devices were undetermined. +6a. **Given** an advisory covering only one product family, **When** the analyst reads its row, **Then** the row's population is limited to inventory devices of that family, and devices of other families appear in neither its impacted, unaffected, nor undetermined figures. +6b. **Given** an advisory whose covered families cannot be established from either Cisco's answers or its published product list, **When** the analyst reads its row, **Then** every undetermined device in the inventory appears in that row rather than being omitted. +7. **Given** the assessment found advisories affecting the inventory that the bundle does not list, **When** the analyst reads the report, **Then** the report states how many such findings the bundle filter excluded. +8. **Given** the same inventory and the same bundle, **When** the tool is run twice, **Then** the advisory rows appear in the same order both times. +9. **Given** a completed run, **When** the analyst opens the report with no network access, **Then** it renders completely, and no existing report was overwritten to produce it. + +--- + +### User Story 2 - Run a bundle review unattended (Priority: P2) + +A scheduled job, or an analyst working through several bundles in succession, supplies the Event Response URL up front instead of typing it at a prompt. The tool performs the same review with no interaction and exits with a status the caller can act on. + +**Why this priority**: The project's CLI-first principle means a prompt cannot be the only way in — a capability reachable only by typing is not scriptable, and bundle reviews are exactly the recurring work a team wants scheduled. It is separable from Story 1, which is complete and useful interactively, but the product is not correct without it. + +**Independent Test**: Invoke the tool with the bundle URL supplied up front and no terminal attached; confirm it never prompts, writes the same report Story 1 produces for the same inputs, and returns a success status. Then invoke it with an unresolvable URL and confirm a distinct non-zero status and no report. + +**Acceptance Scenarios**: + +1. **Given** the bundle URL is supplied up front, **When** the tool runs with no terminal attached, **Then** it never prompts and writes the same report the interactive path produces for the same inputs. +2. **Given** the bundle URL is supplied up front and the bundle cannot be resolved, **When** the tool runs, **Then** it exits with a non-zero status distinguishable from a successful run and writes no report. +3. **Given** no bundle URL is supplied and no terminal is attached, **When** the tool runs, **Then** it produces the whole-fleet report without prompting. + +--- + +### User Story 3 - Skip the bundle and get today's report (Priority: P2) + +A user who has no bundle to hand, or who wants the whole-fleet view, declines at the prompt and gets exactly the report the tool produces today. + +**Why this priority**: Bundle scoping must be an addition, not a tax. Existing users and existing habits have to keep working unchanged, and preserving that is what makes this feature safe to ship. It is testable entirely on its own by comparing against pre-change behaviour. + +**Independent Test**: Run the tool and decline at the bundle prompt; confirm the report produced is the whole-fleet report, with the same verdicts, reasons, and exit status as before this feature. + +**Acceptance Scenarios**: + +1. **Given** the user declines at the bundle prompt, **When** the run completes, **Then** the report is the whole-fleet report and its verdicts and reasons match what the tool produced for the same inputs before this feature. +2. **Given** the user declines at the bundle prompt, **When** the run completes, **Then** no request was made to any bundle page. +3. **Given** a run with no bundle, **When** it completes, **Then** the exit status matches the pre-change behaviour for the same outcome. + +--- + +### User Story 4 - Undetermined devices are actionable, not just flagged (Priority: P2) + +The bundle report carries a section naming every device whose assessment was undetermined, exactly once, with what the inventory recorded, what was asked of Cisco, and why no conclusion was reached — so the inventory data causing the gap can be corrected and the next bundle review comes back conclusive. + +**Why this priority**: Story 1 already prevents an undetermined device from being read as clean, which is the safety requirement. This story makes those devices *fixable* rather than merely visible, converting a recurring caveat into a one-time data cleanup. Valuable and independently testable, but the product is safe without it. + +**Independent Test**: Run a bundle review against an inventory containing releases Cisco does not hold and releases Cisco does not list for the recorded hardware; confirm every such device appears once in a dedicated section with both the recorded values and a human-readable reason. + +**Acceptance Scenarios**: + +1. **Given** 15 devices whose assessment was undetermined, **When** the analyst opens the report, **Then** all 15 are listed once each with a human-readable reason. +2. **Given** a device whose recorded release Cisco does not list for its recorded hardware, **When** the reader inspects its entry, **Then** the entry shows both what the inventory recorded and what was asked of Cisco, so the field to correct is evident. +3. **Given** a device that appears in this section, **When** the reader looks for it in the advisory rows, **Then** it appears there as undetermined and nowhere as unaffected. + +--- + +### User Story 5 - Recover from a bad bundle link (Priority: P3) + +A mistyped, stale, or non-bundle URL, or an unreachable Cisco site, produces a clear statement of which failure occurred and a chance to correct it — never a silently empty or misleading report. + +**Why this priority**: The earlier stories make the happy path correct and safe. This one protects against a wasted run and, more importantly, against the failure mode where a bundle resolves to nothing and an empty report reads as good news. It is a quality-of-transition story rather than a capability. + +**Independent Test**: Submit in turn a URL that is not a Cisco Event Response address, a bundle URL while the site is unreachable, and a reachable page containing no advisories; confirm each produces a distinct message, that the first is rejected before any network request, and that none produces a report. + +**Acceptance Scenarios**: + +1. **Given** a URL that is not a Cisco Event Response address, **When** it is submitted, **Then** the tool rejects it before making any network request, names the expected form, and offers another attempt. +2. **Given** Cisco's site cannot be reached, **When** the tool tries to resolve the bundle, **Then** the message identifies the failure as connectivity rather than a bad link, and no report is written. +3. **Given** a reachable page from which no advisory can be identified, **When** the tool reports it, **Then** it says so explicitly, writes no report, and does not proceed with an empty bundle. +4. **Given** any of these failures at the interactive prompt, **When** the user declines to retry, **Then** the tool falls back to the whole-fleet report rather than exiting with no output. + +--- + +### Edge Cases + +- **A bundle listing an advisory for a product family absent from the inventory.** Its row must be present and must make the population it was evaluated against explicit, so a zero does not read as reassurance about products never assessed. +- **A bundle whose advisories predate Cisco's data coverage** (Cisco holds ASA, FMC, FTD and FXOS vulnerability data only from January 2022 onward). The resulting silence must not be presented as a clean result. +- **A bundle page revised since the analyst received the link**, so it now lists a different set of advisories. The report must describe the bundle as resolved at run time. +- **A bundle listing a single advisory**, and a bundle listing far more than 25. Both must render, and the second must not be truncated. +- **An advisory listed twice on one bundle page.** It must produce exactly one report row. +- **Every device in the inventory undetermined.** Every advisory row must show zero impacted and zero unaffected with the whole population undetermined, and the report must not read as a clean bill of health. +- **No device in the inventory belonging to any family the bundle names.** The report must still be produced, with every row explicit about the empty population. +- **A run that stops part-way** because Cisco became unreachable or rejected the credentials after the assessment began. Every answer already obtained must be kept, the report must say the run was partial and why, and unreached devices must be undetermined. +- **An advisory for which Cisco supplies no fixing release.** It must render as explicitly absent — never inferred, and never blank in a way that reads as "none needed". +- **Bundle page content containing markup, script, or styling.** It must not be able to alter the report's structure, styling, or behaviour. +- **A bundle identifier submitted instead of a full URL.** Accepted, per FR-005. +- **An inventory with no rows, or none in a queryable family.** The failure must be the existing inventory failure, not a new bundle-specific one. + +## Requirements *(mandatory)* + +### Functional Requirements + +#### Bundle input + +- **FR-001**: The tool MUST prompt for a Cisco Event Response URL during a normal run, after the inventory has been located and before any request concerning the inventory is made. +- **FR-002**: The tool MUST accept the bundle URL supplied up front, and MUST NOT prompt at all when it is supplied that way. +- **FR-003**: Users MUST be able to decline at the prompt, and the tool MUST then produce the whole-fleet report with behaviour identical to the pre-change product. +- **FR-004**: The tool MUST validate that a submitted value is a Cisco Event Response address before making any network request, and MUST reject anything else with a message naming the expected form. +- **FR-005**: The tool MUST accept the URL in the form Cisco publishes it, and SHOULD additionally accept a bare bundle identifier such as `ERP-75736`. + +#### Resolving the bundle + +- **FR-006**: The tool MUST resolve an Event Response URL to the complete set of advisories that bundle publishes, identified by advisory identifier. +- **FR-007**: The tool MUST use the bundle page **only** to determine which advisories are in scope. It MUST NOT take any advisory fact — title, severity, score, CVE identifier, publication date, fixing release, or affected product — from that page, even when the page displays it. +- **FR-008**: The tool MUST source every advisory fact it displays from Cisco's advisory data, including for advisories in the bundle that impact no inventory device. +- **FR-009**: The tool MUST report what it resolved — bundle identifier, bundle title, and advisory count — before beginning work concerning the inventory, so a wrong link is caught before a run is spent on it. +- **FR-010**: The tool MUST distinguish and report differently these three outcomes: the value is not a bundle address; Cisco could not be reached; the page was reached but no advisory could be identified from it. +- **FR-011**: The tool MUST NOT write a bundle report when the bundle could not be resolved, and MUST exit with a non-zero status a caller can distinguish from a successful run. +- **FR-012**: The tool MUST treat everything retrieved from the bundle page as untrusted input that cannot alter the report's structure, styling, or behaviour, and cannot become executable content. +- **FR-013**: Resolving a bundle MUST be time-bounded, so an unresponsive page fails before the run's expensive work begins rather than stalling it. +- **FR-014**: An advisory appearing more than once on a bundle page MUST yield exactly one advisory in the resolved set. + +#### Filtering and scope disclosure + +- **FR-015**: The tool MUST assess the inventory using the existing rules, then narrow the result to the bundle's advisories. Narrowing MUST NOT change any device's verdict. +- **FR-016**: The tool MUST report how many distinct advisories affecting the inventory were excluded by the bundle filter, so the report cannot be mistaken for a whole-fleet assessment. +- **FR-017**: The tool MUST produce a row for every advisory the bundle lists, including advisories that impact no inventory device. +- **FR-018**: The tool MUST NOT add an advisory to the report that the bundle does not list, regardless of whether it affects the inventory. + +#### Report layout + +- **FR-019**: The report MUST be laid out one row per advisory in the bundle. +- **FR-020**: Each row MUST show the advisory identifier as a link to Cisco's published advisory that opens in a new tab without granting the opened page access to the report. +- **FR-021**: Each row MUST show the advisory's publication date as published by Cisco. +- **FR-022**: Each row MUST name every inventory device Cisco records as impacted by that advisory, and MUST state how many there are. **The device is the unit of count** throughout the report. +- **FR-022a**: Each row MUST additionally roll its impacted devices up by affected release, showing how many distinct releases are affected and which, so a reader sees both the size of the work (devices) and the size of the decision (releases) without doing arithmetic. +- **FR-023**: Each row MUST show, for each impacted product, the release Cisco lists as first fixing that advisory; a fixing release Cisco does not supply MUST render as explicitly absent, never inferred and never blank. +- **FR-024**: Each row MUST render products whose status for that advisory could not be determined in a visually distinct way, using yellow, and MUST additionally label them as undetermined in words so the meaning never depends on colour alone. +- **FR-025**: Each row MUST account for every device in that row's evaluated population exactly once across impacted, confirmed unaffected, and undetermined, so the row's figures can be checked against the population size. **A row's evaluated population is the inventory devices belonging to the product families that advisory covers**, determined per FR-027a. +- **FR-026**: Each row MUST state the size and the product families of the population it was evaluated against, so a row showing no impact cannot be read as reassurance about products that were never assessed. +- **FR-026a**: The report MUST additionally state, once, the totals for the whole run — how many inventory devices were assessed, how many reached no conclusion, and how many questions were put to Cisco — so the run as a whole is auditable independently of any single row's population. +- **FR-027**: An undetermined device MUST appear in every bundle row whose advisory covers its product family, and MUST NOT be omitted from a row merely because its status there is unknown. +- **FR-027a**: The product families an advisory covers MUST be derived as the union of two sources: the families of the inventory devices for which Cisco returned that advisory, and the product families Cisco names in that advisory's own published product list. Where neither source yields a family, the tool MUST treat the advisory as covering **every** queryable family, so an undetermined device is included rather than omitted. +- **FR-027b**: Family derivation MUST NOT be able to turn an undetermined device into an unaffected one, nor exclude a device Cisco positively listed as impacted. It may only narrow which rows an *undetermined* device appears in. +- **FR-028**: The report MUST NOT present, count, or summarise an undetermined device as unaffected anywhere — including in per-row figures, headline totals, and any summary. +- **FR-029**: The report MUST order advisory rows deterministically, so two runs over identical inputs produce identical document order. +- **FR-030**: The report MUST list every undetermined device once, giving the hardware and release the inventory recorded, what was asked of Cisco, and a human-readable reason. +- **FR-031**: The report MUST identify the bundle it covers — identifier, title, and the bundle's own publication date — and link to the bundle page. +- **FR-032**: The report MUST state that it is bundle-scoped, distinguishably from the whole-fleet report, so the two cannot be confused once filed or emailed. +- **FR-033**: The report MUST remain a single self-contained file that renders with no network access, and MUST NOT overwrite an existing report. +- **FR-034**: The report SHOULD record the set of advisory identifiers as resolved at run time, so a reader can audit the scope the report claims. +- **FR-035**: The report SHOULD allow a reader to filter and sort advisory rows, at least by severity and by number of impacted products. +- **FR-036**: The report SHOULD provide a per-device cross-reference, so a reader can go from one device to every bundle advisory affecting it. + +#### Cross-cutting + +- **FR-037**: Resolving a bundle MUST transmit no inventory-derived data. Nothing about any device may be sent to the bundle page. +- **FR-038**: A run that stops early MUST keep every answer already obtained, report that it was partial and why, and leave unreached devices undetermined — matching the existing behaviour. +- **FR-039**: Bundle resolution and filtering MUST be usable without a terminal: they MUST NOT print, prompt, consult the working folder, write a file, or exit the process. Prompting, progress output, and report writing remain the terminal program's responsibility. +- **FR-040**: A failure to *reach* Cisco during bundle resolution MUST surface as an error, never as an empty bundle or a set of undetermined verdicts, so a caller can tell "we could not ask" from "we asked and could not decide". +- **FR-041**: A bundle-scoped run MUST stay within Cisco's published request quotas, and SHOULD add no more requests to a whole-fleet run than the number of bundle advisories that impact nothing. +- **FR-042**: Behaviour MUST be identical on macOS, Windows, and Linux. + +### Key Entities + +- **Bundle**: The set of advisories one Cisco Event Response page publishes. Carries an identifier (`ERP-#####`), a title, its own publication date, and the resolved set of advisory identifiers. Resolved fresh on every run; never persisted between runs. +- **Bundle advisory row**: One advisory in the bundle as presented in the report. Carries the advisory identifier and its link, the advisory's publication date, its severity and score, its CVE identifiers, the product families it covers, three device groupings — impacted, confirmed unaffected, undetermined — the size of the population it was evaluated against, and the rollup of its impacted devices by affected release. +- **Covered family set**: The product families one advisory applies to, as the union of the families Cisco returned that advisory for and the families named in its published product list. Determines a row's population. Fails open to every queryable family, and can only narrow which rows an *undetermined* device appears in — never suppress an impacted device or create an unaffected one. +- **Impacted product entry**: One device within an advisory row that Cisco records as affected. Carries the device as the inventory names it, its hardware and release, and the release Cisco lists as first fixing that advisory. +- **Undetermined product entry**: One device within an advisory row whose status for that advisory could not be established, because the device's own assessment reached no conclusion. Carries the device as the inventory names it, and is never resolvable to impacted or unaffected without new data. +- **Suppressed finding**: An advisory affecting the inventory that the bundle does not list, and which is therefore absent from the report. Counted and disclosed, not itemised. +- **Assessment result**: The existing per-device outcome — verdict, reason, and the advisories Cisco returned — produced unchanged by this feature and consumed by the filter. Not redefined here. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: An analyst goes from a pasted Event Response URL to a complete per-advisory impact report in a single command, transcribing no advisory identifiers and cross-referencing nothing by hand. +- **SC-002**: For a bundle listing 25 advisories, the report contains exactly 25 advisory rows; the set matches the bundle page exactly, with none missing and none invented. +- **SC-003**: Every advisory row's impacted, unaffected, and undetermined figures sum to that row's stated population, for every row, so the report is internally checkable; and the whole-run totals are stated once so the run is auditable independently of any row. +- **SC-004**: No device with an undetermined assessment appears as unaffected anywhere in the report; each is present in every row it could belong to, and listed once with a reason. +- **SC-005**: Every advisory fact rendered in the report is present in Cisco's advisory data for that advisory; none originates from the bundle page. +- **SC-006**: A user who declines the bundle prompt receives a report equivalent in content to the one the tool produced before this feature, for the same inputs. +- **SC-007**: Each of the three bundle-resolution failures produces a distinct message, and none produces a report. +- **SC-008**: A bundle review of a 70-device inventory adds no more than 25% to the elapsed time of a whole-fleet run over the same inventory, and stays inside Cisco's per-minute and per-day request quotas. +- **SC-009**: A reader can tell from the report alone that it is bundle-scoped rather than whole-fleet, and can see how many findings the bundle filter excluded. +- **SC-010**: The report opens and renders completely with no network access, and no run overwrites an earlier report. + +## Assumptions + +The first five were verified against the live sources on 2026-07-28, using ERP-75736 and the current 70-row inventory, rather than assumed from documentation. + +- **A bundle's advisory list is machine-readable from its page.** The page responds quickly and yields exactly the advisories it lists, via the advisory links in its table. The identifiers are **not** present in the page's visible text, so reading the text alone is insufficient. +- **The advisory identifier and publication date are already in the data the tool retrieves per device.** Filtering an assessment to a bundle therefore costs no additional requests, and the publication date needs no separate lookup for any advisory that impacts at least one device. +- **An advisory in the bundle that impacts nothing needs its own lookup.** Such an advisory appears in no device's result, so its title and publication date must be retrieved individually. Measured at 1 of 25 for ERP-75736; the worst case is the bundle size. +- **Cisco publishes no bundle endpoint.** Resolving a bundle to its advisories requires reading the published page; there is no equivalent in Cisco's advisory data. +- **A device whose assessment is undetermined has no advisory list at all.** Cisco never answered for it, so its status against any specific advisory is genuinely unknown: it cannot be placed in an advisory row as a fact, and it cannot be excluded as clean. This is the structural reason FR-024 through FR-028 exist, and the single most important constraint in the feature. +- **A bundle-scoped run assesses the whole inventory and filters afterwards.** Narrowing the assessment itself to the families a bundle names would be an optimisation, but it would depend on interpreting Cisco's product naming, and a family named only in an advisory's prose would be missed. A whole-inventory run over 70 devices costs about 32 requests and 72 seconds, so the optimisation buys little against the risk. +- **Both report layouts coexist.** The whole-fleet report remains the output when no bundle is supplied (FR-003). Whether the bundle layout eventually replaces it is deliberately not decided here; if it ever does, that is a breaking change to the product's primary output. +- **Default row order is severity first, then number of impacted products, with the advisory identifier as a final tiebreak** — chosen to put the most urgent, widest-reaching work first while satisfying the determinism FR-029 requires. Bundle-page order remains an alternative the plan may revisit. +- **Bundle membership is re-resolved on every run**, so a revised bundle page is picked up without a tool release, and no bundle state is cached between runs. +- **Products are identified in the report the way the inventory names them**, so a reader can match a row back to their own records. +- **The existing inventory format, credential handling, verdict vocabulary, and reason kinds are unchanged** by this feature. + +Scope boundaries adopted here, each of which a reader might otherwise assume is included: + +- **One bundle per run.** Reviewing several bundles in one report is not included. +- **Only Event Response bundle addresses are accepted inputs.** Individual advisory pages, security bulletins, and non-Cisco vulnerability feeds are not. Answering "does this one advisory affect us?" for a single advisory or CVE uses the same mechanism and is the natural successor feature, but is out of scope here. +- **Rows are per advisory, never per CVE.** An advisory's CVE identifiers appear within its row; a bundle where several advisories share a CVE is not decomposed. +- **No verdict rule changes.** Release normalisation, model-to-platform mapping, and the verdict decision table are untouched; this feature filters and re-renders an assessment rather than producing one. +- **No state between runs.** No record of what was patched since the last bundle, no ticket integration, no history. Each report is a point-in-time record. +- **No offline bundle cache.** The bundle page is resolved per run; snapshotting bundles for air-gapped use is not included. +- **The existing whole-fleet report is neither removed nor restyled.** +- **Which advisories *should* have been in a bundle is not inferred.** A bundle's membership is whatever Cisco publishes on that page at run time. + +### Dependencies + +- **Cisco's PSIRT advisory data and its platform and release catalogues** — input; the sole source of every advisory fact and every verdict. Unchanged by this feature. +- **Cisco Security Center Event Response pages** — input; supply the set of advisories in a bundle. **New external endpoint.** The project's constraints treat any additional endpoint the analyzer itself contacts as a reviewed change, so the planning phase must record it, and the required amendment, before implementation — the same gate the advisory API endpoints passed through for spec 006. +- **The existing assessment capability** (spec 006) — input; produces the per-device result this feature filters. Reused, never reimplemented. +- **The existing report capability** — this feature adds a second layout alongside it; both must keep the self-containment and untrusted-content guarantees. +- **The user's inventory workbook** — input, unchanged in format. +- **Cisco API credentials** — input, unchanged; the tool must still refuse to run without them. + +### Deferred to planning + +- Whether to cross-check the resolved bundle against Cisco's advisory data by publication date. It would return a superset of a bundle's advisories and could catch a page-parsing regression by disagreeing, at the cost of one request and some complexity. +- Whether the version bump is MINOR or MAJOR, which follows from the coexistence decision above and from whether the shared surface gains or changes. +- How a row with many impacted devices is presented without truncating it — one advisory in the reference bundle impacts 50 of 70 devices, and FR-033's self-containment rules out loading anything on demand. From 8848fb59859f29874c4c6f54cf0e77e1311cd999 Mon Sep 17 00:00:00 2001 From: Ali Bahaloo Date: Tue, 28 Jul 2026 13:12:14 -0700 Subject: [PATCH 3/7] docs(008): plan, research, data model and contracts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../contracts/bundle-report.md | 141 +++++++ .../contracts/bundle-run-cli.md | 119 ++++++ .../contracts/erp-access.md | 133 ++++++ .../data-model.md | 179 ++++++++ specs/008-erp-bundle-scoped-report/plan.md | 277 +++++++++++++ .../quickstart.md | 172 ++++++++ .../008-erp-bundle-scoped-report/research.md | 384 ++++++++++++++++++ 7 files changed, 1405 insertions(+) create mode 100644 specs/008-erp-bundle-scoped-report/contracts/bundle-report.md create mode 100644 specs/008-erp-bundle-scoped-report/contracts/bundle-run-cli.md create mode 100644 specs/008-erp-bundle-scoped-report/contracts/erp-access.md create mode 100644 specs/008-erp-bundle-scoped-report/data-model.md create mode 100644 specs/008-erp-bundle-scoped-report/plan.md create mode 100644 specs/008-erp-bundle-scoped-report/quickstart.md create mode 100644 specs/008-erp-bundle-scoped-report/research.md diff --git a/specs/008-erp-bundle-scoped-report/contracts/bundle-report.md b/specs/008-erp-bundle-scoped-report/contracts/bundle-report.md new file mode 100644 index 0000000..447a2c4 --- /dev/null +++ b/specs/008-erp-bundle-scoped-report/contracts/bundle-report.md @@ -0,0 +1,141 @@ +# Interface Contract: the bundle-scoped HTML report + +The second report layout, alongside the whole-fleet report contracted in spec 006 +(`contracts/html-report.md`). Both are produced from the same assessment and must never disagree +about a device. + +## Output + +| Property | Value | Requirement | +| --- | --- | --- | +| Location | `output/` inside the working folder | unchanged | +| Filename | `impact_report__.html` | FR-032, Decision 10 | +| Overwrite | never; a colliding path selects a new one | FR-033 | +| Self-contained | single file, no external reference of any kind | FR-033 | + +Keeping the `impact_report_` prefix means the constitution's recorded `impact_report_*.html` output +pattern and `.gitignore`'s existing rule both continue to match with no change to either. + +## Document structure + +``` +┌ Bundle header ───────────────────────────────────────────────┐ +│ ERP-75736 · title · bundle published date · link to page │ FR-031 +│ "Bundle-scoped report" stated explicitly │ FR-032 +├ Run totals ──────────────────────────────────────────────────┤ +│ 70 assessed · 15 undetermined · 27 questions asked │ FR-026a +│ N advisories affecting this inventory are outside this bundle │ FR-016 +│ [partial-run cause, when present] │ FR-038 +├ Advisory rows ───────────────────────────────────────────────┤ +│ one row per advisory in the bundle, ordered deterministically │ FR-017, FR-019, FR-029 +├ Undetermined devices ────────────────────────────────────────┤ +│ each device once, with recorded values and reason │ FR-030 +├ Resolved scope ──────────────────────────────────────────────┤ +│ the advisory identifiers as resolved at run time │ FR-034 +└──────────────────────────────────────────────────────────────┘ +``` + +## An advisory row + +| Element | Content | Requirement | +| --- | --- | --- | +| Identifier | `advisoryId`, linked to `publicationUrl`, opens in a new tab with no access to the report | FR-020 | +| Published | `firstPublished`, as Cisco publishes it | FR-021 | +| Severity / CVSS | `sir`, `cvssBaseScore` | — | +| CVEs | in-row; never promoted to their own row | scope boundary | +| Evaluated against | population size **and** covered families | FR-026 | +| Impacted | every device named, with its first-fix release | FR-022, FR-023 | +| Release rollup | device count · affected releases · fixing release | FR-022a | +| Undetermined | every applicable device, yellow **and** labelled in words | FR-024, FR-027 | +| Counts | impacted / unaffected / undetermined | FR-025 | + +### Link safety + +The identifier link must open in a new tab **and** deny the opened page access to the report +document. The whole-fleet report already establishes this behaviour (commit `544629c`, "open +advisories in a new tab"); this layout matches it. + +### The accounting invariant + +Every row satisfies: + +``` +impacted + unaffected + undetermined == population +``` + +This is the report's own self-check and is directly assertable in tests for every row (FR-025, +SC-003). + +### Long rows + +A row whose device list exceeds the display threshold is placed inside the existing +`
`/`` disclosure, with **every** device name present in the document. Collapsing +is required; truncation is forbidden — one advisory in the reference bundle impacts 50 of 70 devices, +and a hidden device is an unpatched device (research Decision 11). + +### A row that impacts nothing + +Present, never omitted (FR-017). It states that no inventory device is impacted, names the +population and families it was evaluated against, and still shows its undetermined count. A bare +"0 impacted" is not sufficient: without the population it reads as reassurance about products that +were never assessed. + +## Undetermined rendering — the safety rules + +These implement Principle IV and are the report's non-negotiable behaviour. + +| Rule | Requirement | +| --- | --- | +| Visually distinct, using yellow | FR-024 | +| Labelled in words as well as coloured — meaning never depends on colour | FR-024 | +| Present in every row whose advisory covers the device's family | FR-027 | +| Never counted or summarised as unaffected, anywhere, including totals | FR-028 | +| Listed once in its own section with the recorded values and a reason | FR-030 | +| Included rather than omitted when family derivation yields nothing | FR-027a | + +A reader must not be able to reach a screen, count, or summary in which an undetermined device +appears to be clean. + +## Escaping + +Every interpolated value is escaped at the point of interpolation, whether it came from Cisco's API +or the bundle page: + +| Source | Treatment | +| --- | --- | +| `advisoryTitle`, `cves`, `sir` | escaped (advisory `summary` is known to carry raw HTML) | +| `publicationUrl` | escaped as an attribute value | +| Bundle title, `Doc ID`, dates | escaped — a scraped page is the least trustworthy input | +| Device names, models, releases | escaped | + +No page- or API-supplied value may alter the document's structure, styling, or behaviour, or become +executable content (FR-012). + +## Interaction + +| Capability | Requirement | +| --- | --- | +| Filter and sort rows by severity and impacted count | FR-035 (SHOULD) | +| Per-device cross-reference: one device → every bundle advisory affecting it | FR-036 (SHOULD) | + +Both must work with no network access, so any behaviour is inline. A CDN-hosted table library was +already rejected for the whole-fleet report for exactly this reason (spec 006, Decision 12). + +## Default ordering + +Severity rank (`critical` → `informational`, unknown last), then descending impacted device count, +then advisory identifier. The identifier tiebreak is what makes the order total and therefore +reproducible run to run (FR-029). Reuses the whole-fleet report's existing severity mapping so the +two layouts agree on what severity means. + +## Relationship to the whole-fleet report + +| | Whole-fleet | Bundle-scoped | +| --- | --- | --- | +| Row unit | device | advisory | +| Scope | every advisory affecting the inventory | only advisories the bundle lists | +| Produced when | no bundle supplied | a bundle resolves | +| Verdict rules | identical — one assessment feeds both | identical | + +Neither may report a different verdict for the same device from the same assessment. The layouts +differ only in aggregation and scope. diff --git a/specs/008-erp-bundle-scoped-report/contracts/bundle-run-cli.md b/specs/008-erp-bundle-scoped-report/contracts/bundle-run-cli.md new file mode 100644 index 0000000..3b2c7ad --- /dev/null +++ b/specs/008-erp-bundle-scoped-report/contracts/bundle-run-cli.md @@ -0,0 +1,119 @@ +# Interface Contract: the command-line surface for a bundle-scoped run + +Extends the run contract in spec 006 (`contracts/api-run-cli.md`). Everything not stated here is +unchanged. + +## Invocation + +```bash +# interactive — prompts for the bundle after locating the inventory +caia + +# flag-driven — never prompts +caia --erp https://sec.cloudapps.cisco.com/security/center/viewErp.x?alertId=ERP-75736 + +# a bare bundle identifier is accepted and expanded +caia --erp ERP-75736 +``` + +| Option | Behaviour | Requirement | +| --- | --- | --- | +| `--erp URL\|ERP-#####` | Scope the run to this bundle. Suppresses the prompt entirely | FR-002, FR-005 | +| *(absent, interactive)* | Prompt for a bundle; declining produces the whole-fleet report | FR-001, FR-003 | +| *(absent, non-interactive)* | Whole-fleet report, no prompt | FR-002 | + +Existing options — `--inventory`, `--sheet`, `--output-dir`, `--config`, `--version`, `--update`, +`--uninstall` — are unchanged and compose with `--erp` as they do with each other. + +Principle III requires this dual surface: a capability reachable only by typing is not scriptable, +so `--erp` is a constitutional requirement rather than a convenience. + +## Run sequence + +``` +1. resolve credentials (unchanged — refuses to run without them) +2. locate the inventory (unchanged) +3. resolve the bundle ← new, and BEFORE any advisory request +4. confirm: bundle id, title, N advisories ← new +5. fetch catalogues, assess the inventory (unchanged) +6. metadata lookup for zero-impact bundle advisories ← new +7. filter to the bundle, build rows, write the report ← new layout +``` + +Step 3 precedes step 5 deliberately: a wrong link must cost nothing (FR-001, FR-009). A bundle +failure at step 3 means no advisory request was made, so no quota is spent and no partial state +exists. + +Step 6 runs only for bundle advisories that appeared in no device's result — measured at 1 of 25 for +the reference bundle, worst case the bundle size. + +## Confirmation before the run + +Step 4 reports what was resolved, so a wrong bundle is caught by the person who knows: + +``` +Bundle ERP-75736 — Cisco Event Response: March 2026 Cisco Secure Firewall ASA, +Secure FMC, and Secure FTD Software Security Advisory Bundled Publication +25 advisories · published 2026 March 4 16:00 GMT +``` + +The advisory count is the load-bearing part: it is the check that catches a markup change silently +yielding 24 of 25 (`erp-access.md` § Stability). + +## Prompt behaviour + +| Input | Result | Requirement | +| --- | --- | --- | +| A bundle URL | resolve and proceed | FR-001 | +| A bare `ERP-#####` | expand, resolve, proceed | FR-005 | +| Empty / declined | whole-fleet report | FR-003 | +| Not a Cisco Event Response address | rejected **before** any request; re-prompt | FR-004, FR-010 | +| Anything, when resolution then fails | state which failure; offer another attempt | FR-010 | +| Declining after a failed attempt | whole-fleet report, not an exit with no output | US5 §4 | + +The prompt is never reached when `--erp` is supplied, and never shown in a non-interactive run. + +## Exit codes + +Reuses the existing vocabulary rather than adding to it (research Decision 13). + +| Code | Meaning | +| --- | --- | +| `0` | Report written — including a bundle where nothing is impacted, and a partial run | +| `1` | Bundle unreachable, no advisory identified, `Doc ID` mismatch; or an existing run failure | +| `2` | `--erp` value is not a bundle address; or an existing argument error | +| `130` | Interrupt | + +A malformed value is a bad argument (`2`); a failed fetch is a failed run (`1`). Both are +distinguishable from success, as FR-011 requires. + +**No report is written on any non-zero bundle-resolution outcome** (FR-011). An empty bundle is an +error, never an empty report. + +## Partial runs + +Unchanged from spec 006 (FR-038). If Cisco becomes unreachable or rejects the token *after* the +assessment begins, every answer already obtained is kept, unreached devices are undetermined, the +report is written, and it states that the run was partial and why. Exit stays `0`. + +A bundle-resolution failure is **not** a partial run — it happens before any answer exists, so there +is nothing to keep. + +## Headless boundary (Principle VI) + +| Concern | Belongs to | Requirement | +| --- | --- | --- | +| Resolving a bundle; filtering; building rows | shared surface — no print, prompt, working-folder access, file write, or process exit | FR-039 | +| Prompting, confirmation output, progress, report writing, exit codes | the terminal program | FR-039 | + +A failure to *reach* Cisco surfaces as a raised error, never as an empty bundle or a set of +undetermined verdicts, so a caller can always tell "we could not ask" from "we asked and could not +decide" (FR-040). + +## What is unchanged + +- Verdict rules, verdict vocabulary, reason kinds. +- The whole-fleet report, byte-for-byte equivalent in content for the same inputs (FR-003, SC-006). +- Inventory format and discovery; credential handling and storage. +- Request pacing, and the existing quota discipline (FR-041). +- Cross-platform behaviour (FR-042). diff --git a/specs/008-erp-bundle-scoped-report/contracts/erp-access.md b/specs/008-erp-bundle-scoped-report/contracts/erp-access.md new file mode 100644 index 0000000..e0faa03 --- /dev/null +++ b/specs/008-erp-bundle-scoped-report/contracts/erp-access.md @@ -0,0 +1,133 @@ +# External Interface Contract: Cisco Security Center Event Response pages + +The second external interface this tool depends on, alongside the PSIRT API +(`contracts/psirt-access.md` in spec 006). Every behaviour recorded here was observed against the +live page on 2026-07-28 using ERP-75736. + +## Host + +| Host | Purpose | +| --- | --- | +| `sec.cloudapps.cisco.com` | Resolve a bundle to its advisory identifiers and its own identity | + +This is a **new** external endpoint the analyzer itself contacts. The constitution's Technology & +Data Constraints require a reviewed change; the amendment is a blocking prerequisite (research +Decision 9). + +## Request + +``` +GET https://sec.cloudapps.cisco.com/security/center/viewErp.x?alertId=ERP-##### +Accept: text/html +``` + +- No authentication. This is a public page; it does **not** use the PSIRT credentials. +- **No inventory-derived data is transmitted.** The request carries only the bundle identifier the + user supplied (FR-037, Principle V). +- Time-bounded, and issued before any advisory request (FR-013, research Decision 12). + +Observed: **HTTP 200**, 181,576 bytes, **0.54 s**, with and without a browser user-agent. + +> `curl` was observed to time out against this host while `urllib` succeeded on first attempt. The +> tool therefore makes no assumption that a browser engine or a particular user-agent is required. + +## Response — what may be taken from it + +The page is a **scope selector**, not an authority. This split is the contract's central rule. + +| May be taken | Field | Why permitted | +| --- | --- | --- | +| ✅ | advisory identifiers | *which* advisories are in scope — the bundle's membership | +| ✅ | `Doc ID` | the bundle's own identity | +| ✅ | document title | the bundle's own name | +| ✅ | `First Published` | the bundle's own publication date | +| ✅ | `Version` | the bundle's own revision | + +| MUST NOT be taken | Also displayed on the page | Authority instead | +| --- | --- | --- | +| ✖ | advisory titles | PSIRT API | +| ✖ | CVE identifiers | PSIRT API | +| ✖ | Security Impact Rating | PSIRT API | +| ✖ | CVSS base score | PSIRT API | +| ✖ | affected products / releases | PSIRT API | +| ✖ | fixing releases | PSIRT API | + +The page displays every one of the forbidden fields in its table. Taking them would create a second +source for facts the API already owns, and two sources for one fact eventually disagree (FR-007, +FR-008). + +## Extraction + +Advisory identifiers come from `href` attributes of anchor elements, via the standard-library HTML +parser: + +``` +CiscoSecurityAdvisory/(cisco-sa-[A-Za-z0-9._-]+) +``` + +Observed on ERP-75736: **25 identifiers**, each appearing exactly once in the raw markup. + +A lexical scan of the whole document returned the identical 25, so the two approaches agree today. +The parser is required anyway: an advisory identifier mentioned in prose rather than linked would +silently widen the bundle's scope, and scope is the one thing this page is trusted for. + +> **The identifiers are not in the page's visible text.** They exist only in link targets. Text +> extraction alone yields advisory *titles* and returns zero identifiers — a failure mode that looks +> like an empty bundle rather than an error. + +Bundle identity comes from the same parse pass, over the page's accumulated text: + +| Value | Pattern | Observed | +| --- | --- | --- | +| `Doc ID` | `Doc ID:\s*(ERP-\d+)` | `ERP-75736` | +| `First Published` | `First Published:\s*(…)` | `2026 March 4 16:00 GMT` | +| `Version` | `Version:\s*([0-9.]+)` | `1.0` | +| title | document title element | `Cisco Event Response: March 2026 Cisco Secure Firewall ASA, Secure FMC, and Secure FTD Software Security Advisory Bundled Publication` | + +## Cross-check + +The extracted `Doc ID` MUST equal the `alertId` in the submitted URL. Observed: they agree. + +Disagreement means the request was redirected to a different or generic page, and is a resolution +failure — otherwise the report would confidently name a bundle it did not analyse. + +## Outcomes + +| Outcome | Detection | Exit | Report | +| --- | --- | --- | --- | +| Resolved | ≥1 identifier, `Doc ID` agrees | `0` | written | +| Not a bundle address | rejected **before** any request | `2` | none | +| Unreachable | transport failure or timeout | `1` | none | +| No advisory identified | reachable, zero identifiers | `1` | none | +| `Doc ID` mismatch | cross-check fails | `1` | none | +| Interrupted | — | `130` | none | + +The three failures are reported distinguishably (FR-010), and none produces a report (FR-011). An +empty bundle is an **error**, never an empty report — an empty report reads as good news. + +## Trust + +Everything retrieved is untrusted text. The bundle title and every other page-derived value is +escaped where it is interpolated into the report, and cannot alter its structure, styling, or +behaviour, nor become executable content (FR-012, research Decision 14). A scraped page is a +strictly less trustworthy input than an API response and inherits the stricter treatment the report +already applies to advisory content. + +## Stability + +Bundle membership is re-resolved on every run, so a revised page is picked up with no tool release, +and no bundle state is cached. + +The dependency on Cisco's markup is real and is mitigated rather than eliminated: FR-009 shows the +resolved advisory count to the user *before* the run begins, so a parse that silently returns 24 of +25 is caught by the person who knows the answer. A publication-date cross-check was considered and +rejected — it cannot distinguish a missed advisory from a same-day publication the bundle +legitimately excludes (research Decision 4). + +## What this interface is not + +There is no bundle endpoint in Cisco's advisory API: `GET /security/advisories/v2/eventresponse/ +ERP-75736` returns **HTTP 404**. A publication-date window over the API +(`/all/firstpublished?startDate=2026-03-04&endDate=2026-03-05`) returns 27 advisories — the +bundle's 25 plus two unrelated same-day publications — so it is a superset of a bundle, never a +substitute for one. diff --git a/specs/008-erp-bundle-scoped-report/data-model.md b/specs/008-erp-bundle-scoped-report/data-model.md new file mode 100644 index 0000000..d00855c --- /dev/null +++ b/specs/008-erp-bundle-scoped-report/data-model.md @@ -0,0 +1,179 @@ +# Phase 1 Data Model: Bundle-Scoped Impact Reporting + +**Feature**: 008-erp-bundle-scoped-report · **Date**: 2026-07-28 + +Nothing here redefines an existing entity. The per-device assessment result produced by spec 006 is +consumed unchanged; this feature adds a bundle, a per-advisory aggregation over that result, and the +rules governing both. + +--- + +## 1. Bundle + +The resolved scope of a run. Produced by `erp.py` from a bundle page; never persisted. + +| Field | Source | Notes | +| --- | --- | --- | +| `bundle_id` | page `Doc ID`, cross-checked against the URL's `alertId` | e.g. `ERP-75736`. Disagreement is a resolution failure | +| `title` | page document title | Untrusted text (FR-012) | +| `published` | page `First Published` | The bundle's own date, distinct from any advisory's | +| `version` | page `Version` | e.g. `1.0`. Recorded so a revised bundle is visible | +| `url` | the submitted URL, normalised | Linked from the report (FR-031) | +| `advisory_ids` | `href` attributes of advisory links | Ordered, **deduplicated** (FR-014) | + +### Validation + +| Rule | On failure | Requirement | +| --- | --- | --- | +| URL is a Cisco Event Response address | reject before any network request; exit `2` | FR-004, FR-010 | +| A bare `ERP-#####` is accepted and expanded | — | FR-005 | +| Page is reachable within the fetch timeout | connectivity failure; exit `1`; no report | FR-010, FR-013 | +| `advisory_ids` is non-empty | explicit "no advisories identified"; exit `1`; no report | FR-010, FR-011 | +| page `Doc ID` equals the URL's `alertId` | resolution failure | Decision 2 | + +A `Bundle` never reaches the report unless every rule passes. There is no partially-valid bundle. + +--- + +## 2. Covered family set + +The product families one advisory applies to. Determines a row's population, and **only** that. + +``` +covered(advisory) = families_answering(advisory) ∪ families_named(advisory) + ─── or, if both are empty ─── + every queryable family +``` + +| Source | Derivation | Authority | +| --- | --- | --- | +| `families_answering` | families whose family-scoped question returned this advisory | Cisco's own answer — no inference | +| `families_named` | firewall families recognised in the advisory's `productNames` | Cisco's published product list | + +### Invariants (FR-027b) + +1. Narrowing may only reduce which rows an **undetermined** device appears in. +2. It can never exclude a device Cisco positively listed as impacted. +3. It can never move a device into *confirmed unaffected*. +4. An empty derivation widens to every family; it never empties a population. + +Invariants 2 and 3 hold structurally, because impacted and unaffected are read from the device's own +assessment, never from the family set. The family set is consulted for one purpose only: deciding +whether an undetermined device belongs in a given row. + +--- + +## 3. Bundle advisory row + +One row of the report. Exactly one per advisory in `Bundle.advisory_ids` (FR-017, FR-019). + +| Field | Source | Requirement | +| --- | --- | --- | +| `advisory_id` | `advisoryId` | FR-020 | +| `url` | `publicationUrl` | FR-020 | +| `published` | `firstPublished` | FR-021 | +| `title` | `advisoryTitle` | untrusted text | +| `severity` | `sir` | FR-029 ordering | +| `cvss` | `cvssBaseScore` | supplementary | +| `cves` | `cves` | shown in-row, never as its own row | +| `covered_families` | §2 | FR-026 | +| `population` | inventory devices whose family ∈ `covered_families` | FR-025, FR-026 | +| `impacted` | devices in `population` whose assessment lists this advisory | FR-022 | +| `unaffected` | devices in `population` with a positive not-impacted verdict | FR-025 | +| `undetermined` | devices in `population` whose assessment reached no conclusion | FR-024, FR-027 | +| `release_rollup` | `impacted` grouped by affected release | FR-022a | + +Every advisory field comes from Cisco's API — for rows with at least one impacted device, from the +payload already retrieved; otherwise from a per-advisory lookup (research Decision 4). No field is +ever taken from the bundle page (FR-007, FR-008). + +### The accounting invariant (FR-025) + +``` +len(impacted) + len(unaffected) + len(undetermined) == len(population) +``` + +This must hold for **every** row and is the report's own self-check. It is why the device is the +unit of count: a release-based denominator cannot close, because an undetermined device frequently +has no release Cisco recognises and so cannot be placed in one. + +### Release rollup (FR-022a) + +Presentation only, derived from `impacted`, and never a denominator: + +``` +5 devices · 2 affected releases (7.4.2.3, 7.4.2.4) · fixed in 7.4.4 +``` + +--- + +## 4. Impacted product entry + +| Field | Source | Notes | +| --- | --- | --- | +| `name` | inventory | As the inventory names it, so a reader can match their records | +| `model`, `release` | inventory | The recorded values | +| `first_fix` | `platforms[].firstFixes[].name` for this advisory | Absent renders as explicitly absent, never inferred (FR-023) | + +--- + +## 5. Undetermined product entry + +| Field | Source | Notes | +| --- | --- | --- | +| `name`, `model`, `release` | inventory | The recorded values | +| `reason` | the existing reason kind and detail | Rendered in the report's undetermined section (FR-030) | + +Reason kinds are reused unchanged from spec 006 — `release_unknown`, `release_not_on_platform`, +`model_unmapped`, `field_missing`, `family_unsupported`, `check_incomplete`. This feature adds none. + +Rendering rules: visually distinct using yellow **and** labelled in words, so meaning never depends +on colour (FR-024); counted in neither impacted nor unaffected, anywhere, including totals (FR-028). + +--- + +## 6. Run totals + +Stated once, independent of any row's population (FR-026a). + +| Field | Notes | +| --- | --- | +| `devices_assessed` | every inventory row | +| `devices_undetermined` | across the whole run | +| `questions_asked` | distinct questions put to Cisco | +| `suppressed_advisories` | distinct advisories affecting the inventory that the bundle does not list (FR-016) | +| `partial_cause` | present when the run stopped early (FR-038) | + +`suppressed_advisories` is a count, not an itemisation — it exists so the report cannot be mistaken +for a whole-fleet assessment, not to smuggle the whole-fleet result back in. + +--- + +## 7. Worked example — ERP-75736 against the reference inventory + +70 devices (37 FTD, 28 ASA, 5 FMC), 27 questions, 32 requests, 15 undetermined. + +| Row | Covered | Population | Impacted | Unaffected | Undetermined | Sums | +| --- | --- | --- | --- | --- | --- | --- | +| `cisco-sa-onprem-fmc-authbypass-5JPp45V2` | FMC | 5 | 5 | 0 | 0 | ✅ 5 | +| `cisco-sa-asa-dos-FCvLD6vR` | ASA | 28 | 0 | 17 | 11 | ✅ 28 | +| `cisco-sa-ftd-snort3ssl-FBEKYXpH` | FTD | 37 | 11 | 22 | 4 | ✅ 37 | +| `cisco-sa-asaftd-ikev2-dos-eBueGdEG` | ASA, FTD | 65 | 50 | 0 | 15 | ✅ 65 | + +The critical FMC row demonstrates the counting decision: **5 devices**, rolled up as **2 affected +releases** (7.4.2.3 ×3, 7.4.2.4 ×2), all fixed in **7.4.4**. + +The ASA row demonstrates why undetermined devices cannot be dropped: 0 impacted, but 11 of 28 +devices were never conclusively assessed. Rendered as "no impact" alone it would read as an all-clear +for 11 devices nobody assessed. + +Under the rejected whole-inventory population, every row above would read `70`, and all 15 +undetermined devices would appear in all 25 rows — about 375 markers, of which the FMC row's 15 +would be nonsense, since an ASA appliance cannot run FMC software. + +--- + +## 8. State transitions + +None. A bundle is resolved, used, and discarded within one run; no bundle state, cache, or history +persists between runs. The per-device verdict lifecycle is unchanged from spec 006. diff --git a/specs/008-erp-bundle-scoped-report/plan.md b/specs/008-erp-bundle-scoped-report/plan.md new file mode 100644 index 0000000..71fe828 --- /dev/null +++ b/specs/008-erp-bundle-scoped-report/plan.md @@ -0,0 +1,277 @@ +# Implementation Plan: Bundle-Scoped Impact Reporting + +**Branch**: `008-erp-bundle-scoped-report` | **Date**: 2026-07-28 | **Spec**: [spec.md](./spec.md) + +**Input**: Feature specification from `/specs/008-erp-bundle-scoped-report/spec.md` + +## Summary + +Cisco publishes Secure Firewall vulnerabilities in bundled publications — one Event Response page +listing every advisory released that day. This feature lets a user scope a run to one such bundle by +pasting its URL, and inverts the report from one row per device to **one row per advisory in the +bundle**, each carrying the advisory identifier as a link, its publication date, and the products it +impacts with their fixing releases. + +The technical approach adds no new data source. The advisory identifier and publication date are +already inside the payload the tool retrieves per device, so filtering an assessment to a bundle +costs zero additional requests; only advisories that impact nothing need an individual lookup (1 of +25 for the reference bundle). Cisco publishes no bundle endpoint, so the bundle's membership is read +from its published page — which contributes *scope only*, never an advisory fact. + +The design's load-bearing constraint is Principle IV. A device whose assessment reached no conclusion +has no advisory list at all, so its status against any specific advisory is genuinely unknown: it can +neither be asserted as impacted nor excluded as clean. Every report decision follows from that. + +## Technical Context + +**Language/Version**: Python 3.9+ (matches `requires-python` in `pyproject.toml`) + +**Primary Dependencies**: None new. Standard library only — `urllib.request` (bundle page fetch), +`html.parser` (bundle page parsing, new to this project), `html.escape` (report rendering), `re` +(identifier and metadata extraction), `datetime`/`pathlib` (report naming). Existing runtime +dependencies (`openpyxl`, `python-dotenv`) are untouched. + +**Storage**: None added. A resolved bundle lives in memory for one run and is never persisted — no +bundle cache, no history, no state between runs. The only file written is the report. + +**Testing**: `unittest` + `unittest.mock` (existing suite under `tests/`; baseline **272 tests, OK**). +Bundle parsing, family derivation, and row building are pure or injectable, so all of it is covered +with no credentials and no network — required by the constitution's Tests gate. New: +`tests/test_erp.py`, `tests/test_bundle.py`, `tests/test_bundle_report.py`; extended: +`tests/test_cli.py` (`--erp` parsing, prompt suppression, exit codes), `tests/test_analyzer.py` +(prompt and decline paths). A committed fixture of the reference bundle page keeps parsing tests +offline. + +**Target Platform**: macOS, Windows, Linux (Constitution II). + +**Project Type**: Single-project CLI (installed as the `caia` console script), and an importable +library whose shared surface is a released interface (Principle VI). + +**Performance Goals**: Bounded by Cisco's quota, as before. The bundle page fetch is measured at +**0.54 s**. The assessment is unchanged: 70 devices → 27 questions → 32 requests ≈ 72 s. Zero-impact +metadata adds one request per such advisory at the existing ~2.1 s pacing — 1 request for the +reference bundle, worst case the bundle size (~53 s if a bundle touches nothing). Total added cost +in the measured case: **~3 s on a 72 s run**, well inside SC-008's 25% bound. + +**Constraints**: An undetermined device must never appear as unaffected in any row, count, or summary +(Principle IV, the feature's safety property); the bundle page may supply scope and the bundle's own +identity but no advisory fact; no new runtime dependency (Principle I); bundle resolution must +transmit no inventory-derived data (Principle V); resolution and filtering must be headless +(Principle VI); the report must remain a single offline-capable file that is never overwritten; all +page- and API-supplied content escaped at interpolation; the whole-fleet report unchanged. + +**Scale/Scope**: 3 new modules (`erp.py`, `bundle.py`, `bundle_report.py`; ~700–900 lines, majority +in the renderer), 3 modified modules (`cli.py`, `analyzer.py`, and the README shared-surface table), +3 new test modules + 2 extended, 1 committed page fixture, docs updates in `README.md` and +`docs/index.html`, a `VERSION` bump to 4.2.0, and a constitution amendment. No new directories +beyond `specs/`. + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +Evaluated against the Cisco Advisory Impact Analyzer Constitution **v3.1.0**. One gate is +**conditional** on an amendment that has not yet been applied — see +[Constitution amendment (required)](#constitution-amendment-required). + +- **I. Standard-Library-First & Minimal Dependencies** — PASS. No new runtime dependency. The bundle + page is fetched with `urllib` and parsed with `html.parser`, both standard library. A third-party + HTML parser was considered and rejected: the standard parser extracted all 25 identifiers from the + live page, so nothing is missing that would justify a dependency (research Decision 1). Driving a + real browser was also considered and rejected as unnecessary — `urllib` fetched the page in 0.54 s + on first attempt, despite an earlier `curl` timeout suggesting otherwise. + +- **II. Cross-Platform Parity** — PASS. Fetching and parsing are platform-neutral; report paths reuse + the existing `pathlib` logic; no new per-user path is introduced. Nothing in this feature touches + terminal handling, which is where platform divergence has previously threatened this principle. + +- **III. CLI-First, Scriptable Interface** — PASS. `--erp` makes the capability fully flag-driven, so + a bundle review needs no human input and can be scheduled; the prompt is the interactive + convenience, not the only route in. Exit codes reuse the existing vocabulary — `2` for a value that + is not a bundle address, `1` for unreachable or empty, `130` on interrupt (research Decision 13) — + so no new code has to be learned. Every refusal states which of the three failures occurred and + what to do (FR-010). + +- **IV. Conservative & Traceable Analysis (NON-NEGOTIABLE)** — PASS, and as with spec 006 this + principle drove the design rather than merely permitting it. + - *Insufficient data must not be a guess*: an undetermined device is carried into every row whose + advisory covers its family, rendered yellow **and** labelled in words, and excluded from every + count of impacted and unaffected (FR-024, FR-027, FR-028). The reference inventory makes the + stakes concrete — one row reads 0 impacted, 17 unaffected, **11 undetermined** out of 28. + - *Never presented as unaffected*: the per-row accounting invariant + (`impacted + unaffected + undetermined == population`) is directly assertable for every row, so + a device silently dropped from a row is a test failure rather than a reading error (FR-025). + - *Traceable to official Cisco source data*: every advisory fact comes from the API, including for + advisories that impact nothing; the bundle page contributes only which advisories are in scope + and the bundle's own identity (FR-007, FR-008). Each row links to Cisco's published advisory. + - *Fail closed*: family derivation unions two sources and **fails open to every family** when both + are empty, so its worst outcome is a noisier report, never a suppressed device. FR-027b states + the invariant: derivation may only narrow which rows an *undetermined* device appears in, and can + never suppress an impacted device or manufacture an unaffected one (research Decision 5). + - A single whole-inventory population was the first candidate and was **rejected on this + principle's own grounds**: ~375 undetermined markers across 25 rows, most of them meaningless, + trains a reader to skip the marker the principle depends on. + +- **V. Secrets Hygiene & Data Locality** — PASS. The bundle page request is unauthenticated and + carries **only** the bundle identifier the user supplied — no inventory-derived data of any kind + (FR-037). The PSIRT credentials are not sent to this host. The existing disclosure bound to Cisco's + API (family, platform, release — never a firewall name) is unchanged and not widened. The report + lands in the already-ignored `output/`, and keeping the `impact_report_` filename prefix means + `.gitignore`'s existing rule still matches it (research Decision 10). + +- **VI. Headless Shared Surface** — PASS, with an amendment required to keep the principle's own text + accurate. Bundle resolution and filtering print nothing, prompt for nothing, consult no working + folder, write no file, and never exit the process; prompting, confirmation output, and report + writing stay in the terminal program (FR-039). A failure to *reach* Cisco raises rather than + returning an empty bundle or a set of undetermined verdicts, preserving the "could not ask" vs + "asked and could not decide" distinction the principle requires (FR-040). Two new modules join the + shared surface — `erp` and `bundle` — because the ERP parse and the family derivation are exactly + the fragile, safety-bearing logic a second consumer would otherwise reimplement, and a second, + divergent answer about which advisories are in scope is the harm this principle exists to prevent + (research Decision 7). Principle VI names its four modules explicitly, so extending the surface is + an amendment, not an implementation detail. + +- **Technology & Data Constraints** — **CONDITIONAL** → PASS once the amendment is applied. Two + clauses are implicated: + - *Networking*: the analyzer today contacts only `apix.cisco.com` and `id.cisco.com`, and the + constraint states that "adding any further external endpoint the analyzer itself contacts remains + a reviewed change". `sec.cloudapps.cisco.com` is such an endpoint. The review is this section; + the amendment records the outcome. + - *Source of truth*: "No scraped or cached-as-authoritative alternatives." This feature reads a + published page, so the boundary must be made explicit rather than left to interpretation — the + page is a **scope selector**, contributing which advisories are in scope and the bundle's own + identity, and is forbidden from supplying any advisory fact. That is a narrowing clause, not a + relaxation: it grants exactly what the feature needs and explicitly closes the door on the + page becoming an authority. The page displays advisory titles, CVEs, severities and scores, all + of which the tool must ignore in favour of the API (`contracts/erp-access.md`). + - *Inputs/outputs*: no change needed. The report keeps the recorded `impact_report_*.html` pattern. + +- **Development Workflow & Quality Gates** — PASS (obligations captured as tasks). Bundle parsing, + family derivation, row building and rendering are all verdict-adjacent logic and therefore *must* + have tests; all of it is pure or injectable, so the suite runs with no credentials and no network, + as the gate requires. Principle VI's headless property gets its own tests (prints nothing, does not + exit, does not mutate the caller's inventory, raises when Cisco is unreachable). The README + shared-surface table must be updated in the same pull request, since two modules join the surface. + `README.md` and `docs/index.html` must both document the bundle flow, mirrored in one PR. `VERSION` + moves to 4.2.0 — MINOR, because the shared surface gains and nothing incompatible changes (research + Decision 8). Work proceeds on `008-erp-bundle-scoped-report` off `main` and merges via pull request. + +### Constitution amendment (required) + +**Not yet applied.** One edit to `.specify/memory/constitution.md`, bumping **3.1.0 → 3.2.0** +(MINOR: materially expanded guidance and an extended surface list, with no principle removed or +redefined), with a Sync Impact Report and `Last Amended` set to the merge date. Three clauses: + +1. **Networking** — add `sec.cloudapps.cisco.com` as an endpoint the analyzer itself contacts, scoped + to resolving a bundle to its advisory identifiers and the bundle's own identity, and record that + the request is unauthenticated and carries no inventory-derived data. +2. **Source of truth** — state the scope-selector boundary: a published bundle page MAY supply which + advisories are in scope and the bundle's own identity, and MUST NOT supply any advisory fact. + This keeps "no scraped or cached-as-authoritative alternatives" substantively true rather than + merely technically survived. +3. **Principle VI** — extend the named shared surface from four modules to six, adding `erp` and + `bundle`, and mirror the addition in the Technology & Data Constraints "Consumers" bullet. + +Precedent is exact. Spec 006 amended 2.1.0 → 2.2.0 to admit `apix.cisco.com` and `id.cisco.com` as a +prerequisite of that feature; the mechanism for a standalone governance change is +`chore/constitution-headless-shared-surface` → 3.1.0 (commit `73de536`). + +**Sequencing**: the amendment is a **blocking prerequisite** of the first implementation task, and is +best carried as its own `chore/` branch and pull request so the governance change is reviewed on its +own merits. Until it merges, an implementation that contacts the page would violate the very +constraint that designates the change reviewable. + +**Result (pre-Phase 0)**: CONDITIONAL PASS. Every principle passes; the Technology & Data Constraints +gate and Principle VI's module list both depend on the amendment above, which is scoped, precedented, +and carried as a prerequisite task. No principle is violated and Complexity Tracking is empty. + +**Re-check (post-Phase 1 design)**: PASS, with three design decisions strengthening the gates rather +than weakening them. + +1. *Principle IV*: family derivation was designed to **fail open** (research Decision 5), and the + per-row accounting invariant (data-model.md §3) converts the principle from a reviewer's judgement + into a mechanical test over every row. The design also rejected its own first candidate — a single + whole-inventory population — specifically because ~375 mostly-meaningless undetermined markers + would devalue the signal the principle rests on. That is the principle constraining the design, + which is what it is for. +2. *Principle I*: the final design adds no dependency, and Decision 1 chose the standard-library + parser over an equally-accurate lexical scan for a reason grounded in this feature's threat model + rather than in style — a prose mention of an advisory identifier would silently widen scope, and + scope is the one thing the page is trusted for. +3. *Principle VI*: the module split (`erp` = I/O, `bundle` = pure, `bundle_report` = rendering) + reproduces the existing `psirt`/`matching`/`html_report` roles exactly, so the headless boundary + needs no new reasoning. Putting the filter in `matching.py` was considered and rejected to avoid + blurring a currently-crisp per-device boundary with per-advisory aggregation. + +The amendment remains the single outstanding prerequisite; the other gate obligations (tests for the +new logic, the README shared-surface table, mirrored README/`docs/index.html` updates, the `VERSION` +bump) are ordinary tasks. Complexity Tracking remains empty. + +## Project Structure + +### Documentation (this feature) + +```text +specs/008-erp-bundle-scoped-report/ +├── plan.md # This file (/speckit-plan command output) +├── research.md # Phase 0 output — 15 decisions, all unknowns resolved +├── data-model.md # Phase 1 output — Bundle, rows, the accounting invariant +├── quickstart.md # Phase 1 output — 8 validation scenarios +├── contracts/ # Phase 1 output +│ ├── erp-access.md # the bundle page: what may and may not be taken from it +│ ├── bundle-report.md # the per-advisory report layout +│ └── bundle-run-cli.md # the command-line surface and exit codes +├── checklists/ +│ └── requirements.md # spec quality checklist (16/16) +└── tasks.md # Phase 2 output (/speckit-tasks — NOT created by /speckit-plan) +``` + +### Source Code (repository root) + +```text +caia/ +├── erp.py # NEW · shared surface · the only module that talks to Cisco's +│ # Security Center: fetch + parse a bundle page into a Bundle +├── bundle.py # NEW · shared surface · pure: family derivation, filtering an +│ # assessment to a bundle, building per-advisory rows +├── bundle_report.py # NEW · renders the bundle-scoped HTML report +├── assessment.py # unchanged · shared surface +├── matching.py # unchanged · shared surface +├── psirt.py # unchanged · shared surface +├── inventory.py # unchanged · shared surface +├── cli.py # MODIFIED · --erp option, exit codes +├── analyzer.py # MODIFIED · bundle prompt, confirmation, report selection +├── html_report.py # unchanged · the whole-fleet layout +├── config.py # unchanged +├── ui.py # unchanged +└── version.py # unchanged + +tests/ +├── test_erp.py # NEW · page parsing, cross-check, hostile markup, failures +├── test_bundle.py # NEW · family derivation, FR-027b invariants, row accounting +├── test_bundle_report.py # NEW · escaping, collapse-not-truncate, ordering, undetermined +├── test_cli.py # EXTENDED · --erp parsing, prompt suppression, exit codes +├── test_analyzer.py # EXTENDED · prompt, decline path, report selection +└── fixtures/ + └── erp_page.html # NEW · committed copy of the reference bundle page + +README.md # MODIFIED · bundle flow + shared-surface table (6 modules) +docs/index.html # MODIFIED · mirrored, per the Documentation gate +VERSION # MODIFIED · 4.1.0 → 4.2.0 +.specify/memory/constitution.md # MODIFIED · 3.1.0 → 3.2.0 (prerequisite, own PR) +``` + +**Structure Decision**: Single-project CLI, unchanged. The three new modules take the three roles the +package already uses — I/O, pure logic, rendering — mirroring `psirt.py`, `matching.py` and +`html_report.py` respectively, so the Principle VI boundary needs no new reasoning. `erp.py` and +`bundle.py` join the shared surface; `bundle_report.py` stays outside it, as `html_report.py` does. +No new directories are created beyond this feature's `specs/` folder and the test fixture. + +## Complexity Tracking + +> **Fill ONLY if Constitution Check has violations that must be justified** + +No Constitution Check violations — this section is intentionally empty. The one conditional item +(a new external endpoint, plus extending Principle VI's named surface) is a constraint that +explicitly gates on review rather than a violation; the review is recorded in the Constitution Check +above and the required amendment is carried as a blocking prerequisite task. diff --git a/specs/008-erp-bundle-scoped-report/quickstart.md b/specs/008-erp-bundle-scoped-report/quickstart.md new file mode 100644 index 0000000..511fe98 --- /dev/null +++ b/specs/008-erp-bundle-scoped-report/quickstart.md @@ -0,0 +1,172 @@ +# Quickstart: validating Bundle-Scoped Impact Reporting + +**Feature**: 008-erp-bundle-scoped-report · **Date**: 2026-07-28 + +Runnable scenarios that prove the feature works end to end. Details live in +[data-model.md](./data-model.md) and [contracts/](./contracts/) — this file is the run guide. + +## Prerequisites + +| # | Requirement | Notes | +| - | ----------- | ----- | +| 1 | Cisco PSIRT API credentials | `caia --config`, or `CISCO_CLIENT_ID` / `CISCO_CLIENT_SECRET` | +| 2 | An inventory `.xlsx` (sheet `FW_List`) in the working folder | The 70-row reference inventory reproduces every number below | +| 3 | Reachable `apix.cisco.com`, `id.cisco.com`, `sec.cloudapps.cisco.com` | The third is new to this feature | +| 4 | A bundle URL | Reference: `https://sec.cloudapps.cisco.com/security/center/viewErp.x?alertId=ERP-75736` | + +Install from a checkout: + +```bash +uv tool install --from . caia-cli --force +``` + +## Scenario 1 — Interactive bundle review (US1) + +```bash +caia +``` + +Paste the reference bundle URL at the prompt. + +**Expect** — before any inventory work, a confirmation naming the bundle and **25 advisories**; +then the existing progress output; then a report at +`output/impact_report_ERP-75736_.html`. + +In the report: + +| Check | Expected | Requirement | +| --- | --- | --- | +| Advisory rows | exactly **25** | FR-017, FR-019, SC-002 | +| Rows with ≥1 impacted device | **24** | — | +| Rows with 0 impacted | **1** (`cisco-sa-asa-dos-FCvLD6vR`), present with its population stated | FR-017 | +| `cisco-sa-onprem-fmc-authbypass-5JPp45V2` | 5 devices · 2 affected releases · fixed in 7.4.4 | FR-022, FR-022a, FR-023 | +| Every row's counts | impacted + unaffected + undetermined == population | FR-025, SC-003 | +| Run totals | 70 assessed · 15 undetermined · 27 questions | FR-026a | +| Suppressed count | stated and non-zero | FR-016, SC-009 | +| Undetermined devices | 15, each once, each with a reason | FR-030 | + +## Scenario 2 — Undetermined devices are never clean (US1, US4 — the safety property) + +```bash +caia --erp ERP-75736 +``` + +Inspect the `cisco-sa-asa-dos-FCvLD6vR` row: **0 impacted, 17 unaffected, 11 undetermined**, out of +an ASA population of 28. + +| Check | Expected | Requirement | +| --- | --- | --- | +| The 11 undetermined devices are named in the row | not hidden behind the zero | FR-027 | +| They are yellow **and** labelled in words | remove colour and the meaning survives | FR-024 | +| No count anywhere treats them as unaffected | totals included | FR-028 | +| The row states its population and families | so "0 impacted" cannot read as an all-clear | FR-026 | + +**This is the scenario to run first after any report change.** A row showing "0 impacted" without +its 11 undetermined devices is the feature's worst failure mode. + +## Scenario 3 — Family scoping (FR-025, FR-027a) + +Compare two rows from the same run: + +| Row | Covered families | Population | Requirement | +| --- | --- | --- | --- | +| `cisco-sa-onprem-fmc-authbypass-5JPp45V2` | FMC | 5 | FR-026 | +| `cisco-sa-asaftd-ikev2-dos-eBueGdEG` | ASA, FTD | 65 | FR-026 | + +**Expect** the FMC row to contain no ASA or FTD device in any of its three groupings — not even as +undetermined. If all 15 undetermined devices appear on the FMC row, scoping is not being applied. + +Also confirm the inverse safety property: no device Cisco listed as impacted is ever absent from its +row, whatever the derived family set (FR-027b). + +## Scenario 4 — Non-interactive run (US2) + +```bash +caia --erp ERP-75736 < /dev/null +echo "exit=$?" +``` + +**Expect** no prompt, the same report as Scenario 1, `exit=0`. + +## Scenario 5 — Declining keeps today's behaviour (US3) + +```bash +caia +``` + +Press Enter at the bundle prompt. + +**Expect** the existing whole-fleet report — one row per device — with verdicts and reasons matching +the pre-change product for the same inputs, and **no request to `sec.cloudapps.cisco.com`** +(FR-003, SC-006). + +## Scenario 6 — Bundle resolution failures (US5) + +Each must be distinguishable, and none may write a report (FR-010, FR-011, SC-007). + +```bash +caia --erp https://example.invalid/nope # not a bundle address -> exit 2, no request made +caia --erp ERP-00000000 # reachable, no advisories -> exit 1 +caia --erp ERP-75736 # with the host blocked -> exit 1, connectivity +``` + +| Check | Expected | +| --- | --- | +| The first makes **no** network request | rejected on the value alone (FR-004) | +| The connectivity failure says so | not "bad link" (FR-010) | +| No `output/` file appears for any of the three | FR-011 | +| Interactive equivalents re-prompt, and declining falls back to the whole-fleet report | US5 §4 | + +## Scenario 7 — Offline, non-overwriting, safe (FR-012, FR-033) + +```bash +ls -l output/ +``` + +| Check | Expected | Requirement | +| --- | --- | --- | +| Open the report with networking disabled | renders completely | FR-033 | +| Two runs against the same bundle | two files; neither overwritten | FR-033 | +| Filename | `impact_report_ERP-75736_*.html` | FR-032 | +| Search the file for ` If this reports fewer than the full test count or errors on `openpyxl`, the interpreter is missing +> the runtime dependencies. Use `uv run --with openpyxl --with python-dotenv python -m unittest +> discover -s tests`. Baseline before this feature: **272 tests, OK**. + +Expected new coverage: + +| Area | Must cover | +| --- | --- | +| Bundle page parsing | identifiers from `href`; duplicates collapse to one; identifiers absent from visible text; `Doc ID` cross-check; hostile markup | +| Family derivation | union of both sources; fail-open to every family; the FR-027b invariants | +| Row building | the accounting invariant for every row; zero-impact rows present; release rollup | +| Undetermined handling | never unaffected in any count; present in every covered row; labelled not just coloured | +| Report rendering | escaping of page-derived values; long rows collapsed never truncated; deterministic order | +| Headless property | resolution and filtering print nothing, do not exit, do not mutate the caller's inventory, and raise rather than returning verdicts when Cisco is unreachable | +| CLI | `--erp` parsing, prompt suppression, the three failure exit codes, decline path | + +## Constitutional prerequisite + +The amendment admitting `sec.cloudapps.cisco.com` and extending the shared surface **must merge +before implementation begins** (research Decision 9). Until it does, Scenario 1 cannot legitimately +run — the tool would be contacting an endpoint its own constitution does not permit. diff --git a/specs/008-erp-bundle-scoped-report/research.md b/specs/008-erp-bundle-scoped-report/research.md new file mode 100644 index 0000000..8df50ed --- /dev/null +++ b/specs/008-erp-bundle-scoped-report/research.md @@ -0,0 +1,384 @@ +# Phase 0 Research: Bundle-Scoped Impact Reporting + +**Feature**: 008-erp-bundle-scoped-report · **Date**: 2026-07-28 + +Every observation below was taken against the live sources on 2026-07-28 using ERP-75736 and the +70-row reference inventory. Where a finding contradicts an assumption a reader might reasonably +hold, that is called out, because those are the findings that shaped the design. + +--- + +## Decision 1 — Resolve the bundle page with the standard-library HTML parser, matching on `href` + +**Decision**: Fetch the bundle page with `urllib.request`, feed it to an `html.parser.HTMLParser` +subclass, and collect advisory identifiers from `href` attributes of anchor elements matching +`CiscoSecurityAdvisory/(cisco-sa-…)`. The same single parse pass also accumulates the page's text, +which Decision 2 needs. + +**Rationale**: Measured both candidate approaches against the live page (181,576 bytes, HTTP 200 +in 0.54 s): + +| Approach | Result | +| --- | --- | +| Lexical scan of the whole document | 25 unique identifiers | +| `HTMLParser`, `href` attributes only | 25 unique identifiers | + +They agree exactly, and no identifier appeared in the page's text but not in a link. The parser is +chosen anyway for two reasons that matter beyond this one page: it cannot be fooled by an advisory +identifier mentioned in prose (which would silently widen the bundle's scope, violating FR-007's +"scope only" boundary), and it yields the bundle's own metadata in the same pass instead of +requiring a second regex over raw markup. + +`html.parser` is standard library, so Principle I is satisfied with no new dependency. + +**Alternatives considered**: +- *Lexical scan over raw markup* — equally accurate here, fewer lines, but scope-widening on any + page that names an advisory in prose. Rejected: the failure is silent and enlarges scope. +- *A third-party HTML parser* — no capability the standard library lacks. Rejected under + Principle I. +- *Driving a real browser* — the earlier `curl` timeout suggested the page might need one. It does + not: `urllib` fetched it in 0.54 s on the first attempt, both with and without a browser + user-agent. Rejected as unnecessary and impossible under Principle I. + +--- + +## Decision 2 — Take the bundle's own identity from the page; take nothing else + +**Decision**: Extract four values from the page — `Doc ID` (`ERP-75736`), the document title, +`First Published` (`2026 March 4 16:00 GMT`), and `Version` (`1.0`). Cross-check the extracted +`Doc ID` against the `alertId` in the submitted URL and treat disagreement as a resolution failure. +Take **no** advisory fact from the page, even though the page displays advisory titles, CVE +identifiers, severities, and CVSS scores in its table. + +**Rationale**: FR-031 requires the report to identify the bundle it covers, and Cisco publishes no +API equivalent (Decision 3), so the page is the only source. This is permissible because the +bundle's identity is not an advisory fact — it is the identity of the scope selector. The +distinction is exactly what keeps the constitution's "no scraped or cached-as-authoritative +alternatives" rule intact: the page contributes *which advisories are in scope* and *what that +scope is called*; every fact about an advisory comes from Cisco's API. + +All four values extract cleanly, and the URL's `alertId` agreed with the page's `Doc ID`. The +cross-check is cheap and catches a redirect to a different or generic page — a case that would +otherwise produce a confidently-labelled report about the wrong bundle. + +**Alternatives considered**: +- *Take advisory titles and severities from the page too, saving requests* — rejected outright. + Two sources for the same fact will eventually disagree, and the page is not the authority. It + would also make FR-008 untestable. +- *Derive the bundle title from the URL alone, parsing nothing* — leaves the report unable to name + the bundle in human terms. Rejected: a report an analyst files needs to say which publication it + covers. + +--- + +## Decision 3 — There is no bundle endpoint; the page fetch is unavoidable + +**Decision**: Accept a page fetch as the only way to resolve a bundle, and record +`sec.cloudapps.cisco.com` as a new endpoint requiring a constitution amendment (Decision 9). + +**Rationale**: `GET /security/advisories/v2/eventresponse/ERP-75736` returns **HTTP 404**. Cisco's +advisory API exposes advisories by identifier, CVE, product, OS version, severity, and publication +date, but has no concept of an Event Response bundle. + +**Alternatives considered**: +- *Reconstruct the bundle from a publication-date window* — `GET /all/firstpublished?startDate= + 2026-03-04&endDate=2026-03-05` returns 27 advisories: all 25 the bundle lists, plus + `cisco-sa-clamav-css-Fn4QSZ` and `cisco-sa-webex-xss-TZFTbbwN`, which are same-day publications + belonging to no firewall bundle. So the date window is a **superset**, not the bundle. Using it + as the scope would silently import advisories Cisco did not bundle, and it needs the bundle's + publication date, which itself comes from the page. Rejected as a substitute; see Decision 4 for + why it was also rejected as a cross-check. +- *Ask the user to paste the advisory identifiers* — defeats the feature's whole purpose (SC-001). + +--- + +## Decision 4 — Fetch metadata individually for advisories that impact nothing + +**Decision**: After filtering, for each bundle advisory that appears in no device's result, issue +one `GET /advisory/{advisoryId}`. Do not use a date-window query. + +**Rationale**: An advisory that impacts nothing is never returned by any device-scoped question, so +the report has no title, severity, or publication date for it — yet FR-017 requires its row. The +per-advisory lookup is exact, needs no date semantics, and is naturally bounded: measured at **1 of +25** for ERP-75736, worst case the bundle size. At the existing ~2.1 s pacing that is 2 s typically +and about 53 s in the pathological case where a bundle touches nothing at all. + +Every advisory that *does* impact at least one device already carries `advisoryId`, +`advisoryTitle`, `sir`, `cvssBaseScore`, `cves`, `publicationUrl`, `firstPublished`, +`productNames`, and `platforms[].firstFixes[]` in the payload the tool retrieves today — verified +directly. So the common case costs **zero** additional requests, and the publication date FR-021 +requires needs no lookup at all for those rows. + +**Alternatives considered**: +- *One date-window request covering the whole bundle* — one request instead of N, but it returns a + superset that must then be filtered back to the bundle, and it requires trusting that every + advisory in a bundle shares the bundle's publication date. A re-published advisory carried into a + later bundle would break that. Rejected: more moving parts, a date-semantics assumption, and no + saving in the common case (where N is 0 or 1). +- *Using it as a parse-regression cross-check* (deliberated in the spec) — **rejected**. It cannot + distinguish "the page parse missed an advisory" from "the bundle legitimately excludes a same-day + publication", which is precisely the ambiguity the two extra advisories demonstrate. A check that + cannot fail cleanly is worse than none; FR-009's advisory count shown to the user before the run + is the honest check, because a human recognises 24-instead-of-25 immediately. + +--- + +## Decision 5 — Derive an advisory's covered families from two sources, union, fail open + +**Decision**: The families an advisory covers are the union of: + +1. the families of inventory devices for which Cisco returned that advisory, and +2. the firewall families named in the advisory's own `productNames`. + +If neither source yields a family, treat the advisory as covering **every** queryable family. + +**Rationale**: FR-025's per-row population depends on this, and getting it wrong in the narrowing +direction would suppress an undetermined device — the exact Principle IV failure the feature +exists to prevent. + +Source 1 is authoritative and free: the tool asks Cisco per family, so an advisory returned by the +`fmc` question is one Cisco applies to FMC. No inference. Derived across the reference bundle it +produced clean, unsurprising sets — `[FMC]`, `[ASA, FTD]`, `[FTD]`, `[ASA]`, `[FMC, FTD]`. + +Source 1 alone is insufficient, because an advisory can cover a family that happens to have no +impacted device in *this* inventory. Source 2 closes that gap and is present in the payload already +retrieved (`productNames`), so it also costs nothing. Verified on two advisories: + +| Advisory | `productNames` entries | Firewall families named | +| --- | --- | --- | +| `cisco-sa-ftdfmc-dir-trav-wERgjhWq` | 117 | `FMC`, `FTD` — matches its title | +| `cisco-sa-snort3-multi-dos-XFWkWSwz` | 59 | `FTD` — correctly ignoring the non-firewall products it also lists (Cyber Vision, UTD SNORT IPS Engine) | + +The fail-open rule makes the whole derivation safe by construction: the worst outcome of a failed +derivation is a noisier report, never a suppressed device. FR-027b states the invariant directly — +family derivation may only narrow which rows an *undetermined* device appears in, and can never +suppress an impacted device or manufacture an unaffected one. + +**Alternatives considered**: +- *A single whole-inventory population for every row* — needs no derivation at all and was the + first recommendation. Rejected on safety grounds, not readability: with 15 undetermined devices + across 25 rows it emits roughly **375** undetermined markers, the large majority meaningless (an + ASA appliance marked undetermined against an FMC-only advisory it cannot run). A marker that + appears everywhere is a marker readers learn to skip, which erodes the very signal Principle IV + depends on. Retained instead as the run-level total, FR-026a. +- *`productNames` only* — discards the authoritative source in favour of string interpretation. + Rejected. +- *Mapping families from advisory title keywords* — guessing from prose. Rejected under + Principle IV. + +--- + +## Decision 6 — Order rows by severity, then impacted device count, then identifier + +**Decision**: Sort by Cisco's severity rank, then by descending count of impacted devices, then by +advisory identifier. Reuse the existing `_SEVERITY_ORDER` mapping +(`critical` 0, `high` 1, `medium` 2, `low` 3, `informational` 4, unknown 9). + +**Rationale**: Puts the most urgent, widest-reaching work first, which is the order a change window +is planned in. The identifier tiebreak makes the order total, satisfying FR-029's determinism +requirement — without it, two advisories of equal severity and equal reach could order arbitrarily +between runs. Reusing the existing mapping keeps the two report layouts consistent about what +"severity" means. + +**Alternatives considered**: +- *Bundle-page order* — more directly auditable against Cisco's publication, but buries a critical + advisory below a medium one. Rejected for the default; FR-035's sort controls let a reader reach + any order they want. +- *Impacted count first* — an advisory hitting 50 devices at medium severity would outrank a + critical hitting 5. Rejected: severity is the stronger signal. + +--- + +## Decision 7 — Three new modules, two of them joining the shared surface + +**Decision**: + +| Module | Role | Shared surface | +| --- | --- | --- | +| `caia/erp.py` | the only module that talks to Cisco's Security Center: fetch and parse a bundle page into a `Bundle` | ✅ | +| `caia/bundle.py` | pure: family derivation, filtering an assessment to a bundle, building rows | ✅ | +| `caia/bundle_report.py` | render the bundle-scoped HTML report | ✖ | + +**Rationale**: This mirrors the split the package already uses and that Principle VI codifies — +`psirt` performs I/O, `matching` is pure and owns the safety property, `html_report` renders. Each +new module takes exactly one of those roles, so nothing new has to be reasoned about. + +`erp.py` and `bundle.py` belong *in* the shared surface. The ERP parse and the family derivation +are precisely the fragile, safety-bearing logic that a second consumer would otherwise +reimplement — and a second, divergent answer about which advisories are in scope is the same class +of harm Principle VI exists to prevent. `bundle_report.py` stays outside it, like `html_report.py`. + +Filtering was considered for `matching.py`, which already owns the verdict rules. Rejected: +`matching` is cohesive around *per-device* verdicts, and this logic aggregates *per advisory*. +Adding it there would blur a boundary that is currently crisp. + +**Alternatives considered**: +- *One `bundle.py` doing fetch, parse, filter, and render* — fewer files, but mixes I/O with pure + logic and so makes the pure part untestable without mocking a network call. Rejected: the + existing `psirt`/`matching` separation exists for exactly this reason. +- *Keeping both new modules out of the shared surface* — avoids amending Principle VI's module + list, but leaves a second consumer to reimplement the ERP parse. Rejected as the wrong trade. + +--- + +## Decision 8 — Product version 4.2.0 (MINOR) + +**Decision**: Bump `VERSION` 4.1.0 → **4.2.0**. + +**Rationale**: The shared surface *gains* two modules and gains no incompatible change; the +existing surface contract (`analyze`, `load_catalogues`, `families_in`, `describe_check`, the +verdict vocabulary, the `psirt` client and its error types, `load_inventory`, the device record +shape) is untouched. The whole-fleet report and every existing command-line behaviour are +unchanged, and a user who supplies no bundle sees no difference. Under the constitution's +versioning gate — "MINOR for an addition, MAJOR for an incompatible change" — that is MINOR. + +MAJOR would be correct only if the bundle layout replaced the whole-fleet report. It does not +(FR-003), which is what keeps this bump small. + +--- + +## Decision 9 — Constitution amendment 3.1.0 → 3.2.0 (MINOR), required before implementation + +**Decision**: Amend `.specify/memory/constitution.md` in one edit before any implementation task +begins. Three clauses: + +1. **Networking** — add `sec.cloudapps.cisco.com` as an endpoint the analyzer itself contacts, + scoped to resolving a bundle to its advisory identifiers and the bundle's own identity, and + record that the request carries no inventory-derived data. +2. **Source of truth** — record the boundary explicitly: a published bundle page may supply *which + advisories are in scope* and the bundle's own identity, and MUST NOT supply any advisory fact. + This is what keeps "no scraped or cached-as-authoritative alternatives" true rather than merely + technically survived: the page is a scope selector, never an authority. +3. **Principle VI** — extend the named shared surface from four modules to six, adding `erp` and + `bundle`, and mirror the addition in the Technology & Data Constraints "Consumers" bullet. + +**Rationale**: The constitution's Networking bullet states that "adding any further external +endpoint the analyzer itself contacts remains a reviewed change", and Principle VI names its four +modules explicitly, so adding to the surface is an amendment rather than an implementation detail. +A new section and materially expanded guidance with no principle removed or redefined is MINOR. + +The precedent is exact: spec 006 amended 2.1.0 → 2.2.0 to admit `apix.cisco.com` and +`id.cisco.com`, and the mechanism for a standalone governance change is +`chore/constitution-headless-shared-surface` → 3.1.0 (commit `73de536`). + +**Alternatives considered**: +- *Route the bundle fetch through something already permitted* — nothing permitted serves the page; + the API returns 404 for bundles (Decision 3). Not available. +- *Amend after implementation* — the constitution designates it a **reviewed** change, so the review + precedes the work. Carried as a blocking prerequisite task. +- *Treat the page as a "source of truth" alongside the API* — would weaken Principle IV by admitting + a scraped authority. Rejected in favour of the narrower scope-selector clause, which grants + exactly what the feature needs and nothing more. + +--- + +## Decision 10 — Report filename keeps the existing prefix and embeds the bundle identifier + +**Decision**: Write `impact_report_ERP-75736_.html`, reusing the existing +`FILENAME_PREFIX = "impact_report_"` and the existing never-overwrite path selection. + +**Rationale**: FR-032 requires the two layouts be distinguishable "once filed or emailed", which +makes the filename part of the requirement rather than only the document body. Embedding the +bundle identifier satisfies that while keeping the `impact_report_*.html` shape, so the +constitution's Inputs/outputs bullet and `.gitignore`'s existing `impact_report_*.html` rule both +continue to match with no change to either. The existing timestamped, collision-avoiding path +logic is reused unchanged, preserving FR-033's never-overwrite guarantee. + +**Alternatives considered**: +- *A new `bundle_report_*.html` prefix* — clearer at a glance, but falls outside both the + constitution's recorded output pattern and `.gitignore`, so it would need a second amendment + clause and risks committing a generated report. Rejected for a worse cost/benefit. +- *Same filename, distinguished only inside the document* — fails "once filed or emailed". + +--- + +## Decision 11 — Collapse long device lists with the existing disclosure pattern + +**Decision**: Render a row's device list inside the existing `
`/`` disclosure +when it exceeds a threshold, with **every** device name present in the document. + +**Rationale**: One advisory in the reference bundle impacts 50 of 70 devices, and FR-033's +self-containment forbids loading anything on demand — so the choice is collapse or truncate, and +truncation would hide devices that need patching. The report already solves exactly this problem +for the inverse case (a device with 80+ advisories) using `
`, and its test suite already +asserts that collapsing never truncates. Reusing the pattern keeps one behaviour, one style, and +one test strategy. + +--- + +## Decision 12 — Resolve the bundle before the inventory work, with a short timeout + +**Decision**: Resolve the bundle immediately after the inventory is located and before the first +Cisco advisory request, with a fetch timeout well below the API client's 45 s. + +**Rationale**: FR-001 and FR-009 already require resolution to precede the run so a wrong link is +caught before minutes are spent. A short bound makes that promise real: measured at 0.54 s, so a +generous ceiling still fails fast. This also means a bundle failure costs the user nothing — +no requests spent, no partial state. + +--- + +## Decision 13 — Reuse the existing exit-code vocabulary + +**Decision**: Map the three bundle-resolution failures onto codes the tool already uses: + +| Outcome | Code | Consistent with | +| --- | --- | --- | +| Value is not a bundle address (bad input) | `2` | existing argument-error convention (`cli.py`) | +| Cisco unreachable, or page yields no advisory | `1` | existing run-failure convention (`analyzer.py`) | +| Interrupt during resolution | `130` | Principle III | + +**Rationale**: FR-011 requires a non-zero status a caller can distinguish from success; Principle III +requires meaningful codes. A malformed URL is a bad argument and a failed fetch is a failed run, so +the existing two codes already carry the right meanings. Inventing a third would add vocabulary +callers must learn for no gain. + +--- + +## Decision 14 — Escape everything the page supplies, at interpolation + +**Decision**: Treat the bundle title, and every other page-derived value, as untrusted text escaped +where it is interpolated into the report — the same treatment advisory content already receives. + +**Rationale**: FR-012 requires it, and the existing report already assumes Cisco-supplied content +is hostile (every advisory observed during spec 006's spike carried raw HTML in `summary`, and the +report's tests assert that a `