Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .specify/feature.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"feature_directory": "specs/007-decommission-ai-analyzer"
"feature_directory": "specs/008-erp-bundle-scoped-report"
}
107 changes: 100 additions & 7 deletions .specify/memory/constitution.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,75 @@
<!--
Sync Impact Report
==================
Version change: 3.1.0 → 3.2.0
Rationale: Enacted as a prerequisite of spec 008 (bundle-scoped impact reporting), which scopes a
run to a Cisco Event Response bundle and reports one row per advisory in it. Cisco
publishes no API equivalent of a bundle — a request for one returns HTTP 404 — so a
bundle's membership can only be learned from its published page. That made two existing
constraints block the feature and one principle's text go stale.

The Networking constraint permitted only `apix.cisco.com` and `id.cisco.com` and stated
that "adding any further external endpoint the analyzer itself contacts remains a reviewed
change"; `sec.cloudapps.cisco.com` is now admitted, with the request explicitly
unauthenticated and forbidden from carrying any inventory-derived data — a stricter bound
than the advisory API's, which may carry the derived family/platform/release triple.

The Source of truth constraint ended "No scraped or cached-as-authoritative alternatives."
Reading a published page could be argued to survive that on a technicality, which is
exactly the kind of reasoning a governance document should foreclose. Rather than relax
the prohibition, a new Scope selectors bullet grants a narrow, enumerated permission: a
bundle page may supply *which* advisories are in scope and the bundle's own identity, and
is forbidden from supplying any advisory fact — title, severity, score, CVE, affected
product, fixing release, or an advisory's publication date — every one of which the page
actually displays. Disagreement resolves in the API's favour without exception, and a page
yielding no advisory is an error rather than an empty scope, because an empty result
rendered as a report reads as an all-clear. This narrows the tool's latitude while
unblocking the feature.

Principle VI named its shared surface as four modules. Spec 008 adds two — `erp` (bundle
resolution) and `bundle` (family derivation, filtering, row construction) — because both
are fragile, safety-bearing logic a second consumer would otherwise reimplement, and a
second divergent answer about which advisories are in scope is the harm the principle
exists to prevent. `bundle_report` joins the terminal-side list beside `html_report`.

A new constraint bullet plus materially expanded guidance, with no principle removed or
redefined and no existing permission widened, so the bump is MINOR. The product version
moves separately to 4.2.0 when the feature lands.

Modified principles:
- VI. Headless Shared Surface (shared surface extended from four modules to six, adding `erp` and
`bundle`; `bundle_report` added to the terminal-side list; the public contract now also names
bundle resolution and covered-family derivation)
Added sections:
- Technology & Data Constraints → Scope selectors (new bullet, with its own rationale)
Modified sections:
- Technology & Data Constraints → Networking (`sec.cloudapps.cisco.com` admitted, scoped to bundle
resolution; the request is unauthenticated and carries no inventory-derived data at all)
- Technology & Data Constraints → Consumers (the released interface now enumerates all six modules)
Removed sections: (none)

Templates requiring updates:
- .specify/templates/plan-template.md ✅ aligned (Constitution Check resolves gates dynamically)
- .specify/templates/spec-template.md ✅ aligned (no constitution references)
- .specify/templates/tasks-template.md ✅ aligned (no constitution references)
- .specify/templates/checklist-template.md ✅ aligned (no constitution references)
- .claude/skills/speckit-*/SKILL.md ✅ aligned (reference the constitution generically)
- README.md ⚠️ pending — the shared-surface table lists four modules and MUST list six; carried as
task T051 of spec 008, in the same pull request as the code (Documentation gate)
- docs/index.html ⚠️ pending — carried as task T052 of spec 008

Follow-up TODOs: none for this document. The two ⚠️ items above are feature tasks, not amendment
debt; the Documentation gate requires them in the pull request that changes the surface.

Previous version 3.1.0 (2026-07-27): enacted after product 4.1.0, which extracted the assessment
into `caia/assessment.py`. Added Principle VI (Headless Shared Surface) naming four modules as a
shared surface, made that surface a released interface, and extended three quality gates to enforce
it. MINOR.
-->

<!--
Superseded Sync Impact Report (3.0.0 → 3.1.0), retained for history
==================
Version change: 3.0.0 → 3.1.0
Rationale: Enacted after product 4.1.0, which extracted the assessment into `caia/assessment.py`
and established four modules — `assessment`, `matching`, `psirt`, `inventory` — as a
Expand Down Expand Up @@ -161,17 +230,21 @@ values means an observer of the tool's traffic learns what software is being ask
nothing about who runs it or where.

### VI. Headless Shared Surface
The modules that produce a verdict — `assessment`, `matching`, `psirt`, and `inventory` — are this
The modules that produce a verdict or determine the scope one is produced for — `assessment`,
`matching`, `psirt`, `inventory`, `erp`, and `bundle` — are this
package's **shared surface** and MUST remain callable without a terminal. They MUST NOT print,
prompt, consult the current working folder, write a file, or exit the process; the caller supplies
the credentials, the inventory, and whatever progress reporting it wants. Terminal behaviour —
prompts, colour, progress output, inventory discovery, report writing, exit codes — belongs to
`cli`, `analyzer`, `ui`, `config`, `version`, and `html_report`, which are outside the shared
`cli`, `analyzer`, `ui`, `config`, `version`, `html_report`, and `bundle_report`, which are outside
the shared
surface and MUST NOT be required by any consumer other than this CLI. Anything that consumes this
package MUST reuse the shared surface rather than reimplement any part of it. The surface's public
contract — `analyze`, `load_catalogues`, `families_in`, `describe_check`, the verdict vocabulary
and reason kinds, the `psirt` client and its error types, `load_inventory`, and the shape of a
device record — MUST NOT change incompatibly without a MAJOR product version bump. A shared-surface
and reason kinds, the `psirt` client and its error types, `load_inventory`, the resolution of a
published bundle to the advisories it contains, the derivation of an advisory's covered product
families, and the shape of a device record — MUST NOT change incompatibly without a MAJOR product
version bump. A shared-surface
function MUST report a failure to *reach* Cisco as an error rather than as a set of undecided
verdicts, so a caller can always tell "we could not ask" apart from "we asked and could not decide".

Expand Down Expand Up @@ -201,6 +274,20 @@ data-quality problem when the real cause was a rejected credential.
platform/release catalogues those lookups are validated against. Catalogues MUST be retrieved
from Cisco rather than hardcoded, so newly published releases and platforms are picked up
without a tool release. No scraped or cached-as-authoritative alternatives.
- Scope selectors: a published Cisco bundle page (an Event Response page announcing a bundled
advisory publication) MAY be read for two things only — *which* advisories a bundle contains, and
that bundle's own identity (its identifier, title, publication date and revision). It MUST NOT
supply any advisory fact: not a title, severity, score, CVE identifier, affected product, fixing
release, or publication date of an advisory. Every such fact MUST come from the advisory API even
when the page also displays it, and a disagreement between the two is resolved in the API's favour
without exception. A page that cannot be resolved to at least one advisory MUST be an error, never
an empty scope, because an empty result rendered as a report reads as an all-clear.

Rationale: this is what keeps the preceding bullet's prohibition substantive rather than merely
survived on a technicality. Cisco publishes no API equivalent of a bundle, so the page is the only
way to learn a bundle's membership — but membership is a *selector*, not a finding. Granting
exactly that and naming every field it may not supply prevents the page from becoming a second
authority over facts the API already owns, which is the failure Principle IV forbids.
- Inputs/outputs: the inventory is a user-provided `.xlsx` (sheet `FW_List`) located in the
current working folder; the report is written into an `output/` folder inside that same
working folder as a timestamped `impact_report_*.html` file. The report MUST be a single
Expand All @@ -209,7 +296,12 @@ data-quality problem when the real cause was a rejected credential.
naming variation. Advisory content received from Cisco MUST be treated as untrusted text that
cannot alter the report's structure, styling, or behaviour.
- Networking: the analyzer itself contacts only `apix.cisco.com` (Cisco PSIRT advisory data and
platform/release catalogues) and `id.cisco.com` (short-lived access token for that API).
platform/release catalogues), `id.cisco.com` (short-lived access token for that API), and
`sec.cloudapps.cisco.com` (a published bundle page, read solely to resolve a bundle to its
advisory identifiers and its own identity, within the limits of the Scope selectors bullet above).
The bundle-page request is unauthenticated — it MUST NOT carry the API credentials — and it MUST
carry no inventory-derived data whatsoever, not even the derived family/platform/release triple the
advisory API is permitted; the only value sent is the bundle identifier the user supplied.
Version and update checks additionally contact
GitHub — `api.github.com` and `github.com` — solely to discover the latest published
release; they send no inventory or secrets and MUST be best-effort and time-bounded so
Expand All @@ -218,7 +310,8 @@ data-quality problem when the real cause was a rejected credential.
package index); the tool orchestrates this rather than overlaying files in place. Adding
any further external endpoint the analyzer itself contacts remains a reviewed change.
- Consumers: this package is both the `caia` command and an importable library. The shared surface
defined in Principle VI is therefore a **released interface** — a consumer depends on a published
defined in Principle VI — `assessment`, `matching`, `psirt`, `inventory`, `erp`, and `bundle` — is
therefore a **released interface** — a consumer depends on a published
release of it, never on a branch — so that surface MUST be documented in `README.md` and its
changes MUST drive the version bump (see the versioning gate below). Modules outside the shared
surface carry no such guarantee and MAY be reorganised freely.
Expand Down Expand Up @@ -292,4 +385,4 @@ Compliance is verified at review time. Dependent Spec Kit artifacts (plan, spec,
tasks templates and their generated documents) MUST remain consistent with the principles
above; the Constitution Check in the planning workflow enforces this on each feature.

**Version**: 3.1.0 | **Ratified**: 2026-07-14 | **Last Amended**: 2026-07-27
**Version**: 3.2.0 | **Ratified**: 2026-07-14 | **Last Amended**: 2026-07-28
62 changes: 55 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,15 +202,48 @@ on it behaves exactly like the uv install — use it as described in [Run it](#r
caia
```

That's the whole thing. No link to paste, no mode to choose. The tool finds your inventory in
that folder (if there's more than one candidate, it lists them and lets you pick), asks Cisco
which published advisories apply to each firewall's software release on its exact hardware
model, and writes **`impact_report_<date>_<time>.html`** into an **`output`** folder inside
your current folder (created automatically). Open it in any browser.
The tool finds your inventory in that folder (if there's more than one candidate, it lists them
and lets you pick), asks whether you want to scope the report to one Cisco advisory bundle
(press **Enter** to skip), asks Cisco which published advisories apply to each firewall's
software release on its exact hardware model, and writes
**`impact_report_<date>_<time>.html`** into an **`output`** folder inside your current folder
(created automatically). Open it in any browser.

Each run adds a new timestamped file, so previous reports are kept. Press **Ctrl+C** at any
time to cancel cleanly.

### Checking one advisory bundle

Cisco publishes firewall vulnerabilities in **bundled publications** — one Event Response page
listing every advisory released that day. When your team's review is triggered by one of those,
paste its link at the prompt, or skip the prompt entirely:

```bash
caia --erp https://sec.cloudapps.cisco.com/security/center/viewErp.x?alertId=ERP-75736
```

A bare identifier works too: `caia --erp ERP-75736`.

The tool tells you which bundle it found and how many advisories it lists **before** it starts
asking Cisco about your inventory, so a wrong link costs you nothing. The report then flips
round: **one row per advisory in that bundle** rather than one row per firewall, each row
showing the advisory, its publication date, and which of your products it impacts along with
the release that fixes them. Advisories in the bundle that hit nothing still get a row — a
clean result you can point at is worth more than a gap.

Two things worth knowing:

- **It is scoped on purpose.** Advisories affecting your fleet from *other* publications are
deliberately absent, and the report says how many it left out. It is not a whole-fleet
assessment; run `caia` with no bundle for that.
- **Firewalls that couldn't be checked appear in every row they might belong to**, marked in
yellow and labelled *undetermined*. They are never counted as unaffected. If a row says
"0 impacted" it also tells you how many devices nobody could answer for.

Because `--erp` needs no prompt, a bundle review can be scheduled or scripted. Exit status is
`2` if the value isn't a bundle address, `1` if Cisco can't be reached or the page yields no
advisories — and no report is written in either case.

### Understanding the output

Every firewall gets exactly one status:
Expand Down Expand Up @@ -400,14 +433,17 @@ to suppress it everywhere.
| `matching.py` | pure verdict logic: release normalisation, model→platform mapping, verdicts | ✅ |
| `psirt.py` | the only module that talks to Cisco's advisory API | ✅ |
| `inventory.py` | read the inventory workbook into per-firewall records | ✅ |
| `erp.py` | the only module that talks to Cisco's Security Center: resolve a bundle page to its advisory identifiers | ✅ |
| `bundle.py` | pure per-advisory aggregation: covered families, filtering, row construction | ✅ |
| `cli.py` | single entry point; refuses removed options, then dispatches `--help`/`--version`/`--update`/`--uninstall`/`--config` and the run | |
| `analyzer.py` | terminal run flow + interactive prompts (working-folder inventory + `output/`) | |
| `html_report.py` | render the self-contained HTML report | |
| `html_report.py` | render the self-contained HTML report, one row per firewall | |
| `bundle_report.py` | render the bundle-scoped report, one row per advisory | |
| `config.py` | per-user configuration (paths, precedence, `--config`) | |
| `version.py` | installed version, GitHub release discovery, uv-based update | |
| `ui.py` | terminal colors + prompts (no dependencies) | |

**Shared surface.** The four modules marked above are the ones a consumer other than this
**Shared surface.** The six modules marked above are the ones a consumer other than this
CLI is expected to import; the rest are the terminal program built on top of them. The
entry point is `assessment.analyze(inventory, client)` — it takes a list of per-device
dicts (whatever produced them) plus a `psirt.Client` the caller owns, prints nothing,
Expand All @@ -417,6 +453,18 @@ in. The split exists so that a second consumer cannot drift from this tool's ver
the rules that read Cisco's data live in one place, and anything that reimplements them
reimplements the safety property in Constitution IV.

For a bundle-scoped run, `erp.resolve(reference)` returns the bundle and
`bundle.build_rows(bundle.advisory_ids, devices)` turns a completed assessment into one row
per advisory. Both are headless for the same reason: the bundle-page parse and the
covered-family derivation are the fragile parts, and a second consumer that reimplemented
them would produce a second, divergent answer about which advisories are even in scope.

**One rule about the bundle page.** It is a *scope selector*, never an authority. It supplies
which advisories a bundle contains and the bundle's own identity — nothing else. Every advisory
fact in the report (title, severity, score, CVEs, publication date, fixing release) comes from
Cisco's API, even though the page displays all of them too. The constitution states this
directly under Technology & Data Constraints → Scope selectors.

The version is single-sourced from the committed `VERSION` file into the package metadata and
reported at runtime via `importlib.metadata`. Devices sharing a product family, hardware
platform and software release collapse into one question to Cisco, so a large inventory costs
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.1.0
4.2.0
Loading
Loading