feat(009): Affected Products column — Cisco's own scope statements (4.3.0) - #19
Merged
Merged
Conversation
…on 3.3.0) BRD-006 shows, beside each advisory in a reviewed bundle, Cisco's own published "Vulnerable Products" and "Products Confirmed Not Vulnerable" statements. Neither exists in the advisory API, so two constraints blocked the feature: - Networking admitted sec.cloudapps.cisco.com only for a bundle page "read solely to resolve a bundle to its advisory identifiers and its own identity". Per-advisory documents exceed "solely". - Source of truth named only the openVuln API and ended "No scraped or cached-as-authoritative alternatives". A CSAF document is neither scraped nor cached, and is reached by following the pointer the API itself returns (csafUrl) rather than a URL the tool invents — but it was not named, and a governance document should not be satisfied on that technicality. Both are amended narrowly. The new Advisory scope statements bullet is bounded to the two statements, requires the API-supplied pointer, requires the document to identify the advisory it was fetched for, forbids the statements from informing any determination, and resolves disagreement with the API in the API's favour. It adds a faithful-rendering obligation — no summarising or paraphrasing, every alteration enumerated and tested — as the mirror of Principle IV's ban on inventing an advisory fact. Scope selectors is deliberately not relaxed; both bullets now state that neither widens the other. Recorded explicitly: this reverses part of the spec-007 decommission, under narrower terms (no on-disk retention, no temporary folder, no AI processing). Principle VI gains the capability in its public contract; the Tests gate gains a per-normalisation obligation. Product version moves to 4.3.0 when the feature lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Converts BRD-006 into spec 009: 4 prioritized user stories, 47 functional requirements, 12 measurable outcomes, 10 edge cases, no open clarifications. The publication date moves into the Advisory column beside the CVSS score; the freed column becomes "Affected Products" carrying Cisco's own Vulnerable Products and Products Confirmed Not Vulnerable statements; the third column becomes "Your impacted devices" so Cisco's product scope cannot be mistaken for a finding about the reader's fleet. Verdicts, counts and the whole-fleet report are unchanged. Six defaults were adopted for questions BRD-006 left open (date value retained, links kept as text, extract shows every sub-heading with its opening paragraph, zero-count stated, Details not shown, no bundle-wide aggregate) and recorded in Assumptions rather than deferred. Validation rewrote five requirements that could not have failed a test. Two changed behaviour rather than wording: FR-031 dropped its undefined "long enough to dominate its row" threshold, so the disclosure control now appears if and only if text lies beyond the extract; FR-007 now requires the report to state the column's provenance rather than requiring it to be "evident". Clarification added five requirements in the one area the BRD treated only as a cost question — how retrieval behaves over a network: - FR-043 retry once on transport failures only; the missing-document case is a success with an empty body, so retrying it is waste. Attempts capped at two. - FR-044/FR-045 a short per-row cause from a closed set of five, so a local network problem is separable from a change on Cisco's side. - FR-046 sequential requests with a courtesy delay; Cisco publishes no allowance for this host, and a throttled burst would mass-mark statements unavailable. - FR-047 progress reported through the caller-supplied mechanism, keeping retrieval headless while avoiding a silent pause that reads as a hang. Adding the retry made FR-039 self-inconsistent — it bounded time per advisory while permitting two attempts, and its aggregate target was unachievable on a degraded network. It now bounds each attempt and scopes the one-quarter target to runs that succeed without retry, while still requiring a degraded run to complete and write its report. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 0 research and Phase 1 design for spec 009. Every figure below was measured
against live Cisco sources across 29 advisories, not estimated.
Retrieval gets its own shared-surface module, caia/csaf.py, rather than extending
an existing one. Both alternatives are worse for reasons stronger than tidiness:
erp.py is deliberately built with no route to supplying advisory facts ("the
cheapest way to keep a rule is to leave no route to breaking it"), so extending it
would dismantle a safeguard to avoid creating a file; and psirt.py owns the PSIRT
credentials that FR-022 forbids this request from carrying. Consequence: a PATCH
amendment (3.3.0 -> 3.3.1) adding `csaf` to Principle VI's enumeration. The
criterion already reaches it, so only the name is missing.
FR-040 is worth honouring. Streaming each response and stopping once the top-level
`document` object parses transfers 3.4% of the bytes: 424 KB instead of 12.9 MB for
the reference bundle, 23 KB instead of 1.74 MB for the largest advisory. It needs no
JSON repair — raw_decode raises while the value is incomplete, so the loop is
accumulate/attempt/read-more. Verified live, including at a hostile 997-byte chunk
size. `document` is the first top-level key in 25 of 25 and carries both the notes
and the tracking identity, so one read serves FR-008, FR-020 and FR-035.
The design also found a defect in the spec. FR-031 required the extract to show
"every sub-heading with its opening paragraph", which breaks on
cisco-sa-ftd-tcp-dos-rHfqnwRg — its sub-headings are diagnostic procedures, not
products, and its scope claim sits above the first heading. The rule therefore
discarded the scope and showed five "Determine whether..." headings in its place:
the exact misrepresentation FR-031 existed to prevent. FR-031 now leads with the
scope claim and lists sub-heading titles as an outline; FR-048 forbids both failure
modes; a User Story 2 scenario covers the procedure-heading shape.
One trap recorded rather than left to be discovered: a partially-read buffer decoded
with errors="ignore" silently drops a multi-byte character split across a chunk
boundary, corrupting text in a feature whose whole point is faithful rendering. The
contract requires strict decoding, treating UnicodeDecodeError as "read more". No
statement in the sample contains non-ASCII, so the path is currently unexercised.
Artifacts: research.md (7 decisions), data-model.md, contracts/csaf-access.md,
contracts/affected-products-column.md, quickstart.md. Constitution gate passes with
two in-PR obligations, both anticipated by 3.3.0's follow-up note: the 3.3.1 PATCH
and the README/docs correction. VERSION moves to 4.3.0 when the feature lands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Discharges the follow-up TODO 3.3.0 recorded against itself. 3.3.0 broadened
Principle VI's criterion to cover "modules that ... retrieve Cisco-published
content a report attributes to Cisco" but could not name the module, because the
design decision had not been taken — naming one then would have let a governance
document pre-decide architecture.
Spec 009's Phase 0 research settled it: retrieval lands in a new module, csaf,
rather than extending either existing candidate. erp is deliberately built with no
route to supplying an advisory fact ("the cheapest way to keep a rule is to leave no
route to breaking it"), so putting content retrieval there would dismantle a
safeguard to avoid creating a file. psirt owns the API credentials that the Advisory
scope statements bullet forbids a scope request from carrying, so hosting an
unauthenticated path there would invert the separation that prohibition creates.
The amendment only makes the enumeration accurate — Principle VI and the Consumers
released-interface list both go from six modules to seven. It adds no permission,
widens no bound, and changes no rule: every obligation now attaching to csaf already
attached under 3.3.0's criterion. Non-semantic refinement, hence PATCH.
Ordering is deliberate: this names a module that does not yet exist, matching 3.2.0,
which added erp and bundle as a prerequisite of the spec that created them. Both land
in one pull request, so main never observes an inconsistent state. README.md and
docs/index.html are deliberately NOT touched here — the Documentation gate forbids
describing a capability the tool lacks, so the shared-surface table changes in the
commit that adds the module.
Plan updated to match: the Principle VI gate now passes rather than pending, and the
remaining documentation obligation is stated as outstanding by design.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 2 output for spec 009. 43 tasks across seven phases: setup, a blocking
foundational phase building the caia/csaf.py shared surface, one phase per user
story in priority order, then the documentation and versioning gates.
Tests are marked NOT optional. Three obligations require them: the constitution's
Tests gate, its per-normalisation clause added in 3.3.0 ("each permitted
normalisation MUST have its own test, and a further test MUST assert that no other
alteration occurs"), and FR-013. They sit beside their implementation task rather
than in an up-front block, because every rule here is provable from recorded
fixtures and so is verifiable immediately rather than accumulating as a batch.
The pure text work (T003-T007) precedes all I/O deliberately. Those rules decide
what the report attributes to Cisco, they need no network or credentials, and if
they are wrong everything downstream renders a faithful copy of the wrong text.
Only 4 [P] markers, deliberately. Most of the feature lands in two files —
caia/csaf.py and caia/bundle_report.py — so marking those parallel would invite
conflicting edits to the same file, which is the failure the marker prevents.
Two tests are called out because each is easy to write so that it passes while the
bug ships: T020 must assert the text *after* <interface_name> survives, not just
that the placeholder renders (unescaped, a browser deletes both and the omission is
invisible); T004 must assert the navigational sentence is removed *and* the
substantive "Only products listed..." sentence is retained — 31 go, 25 that look
alike must stay. T001 must use recorded text, since faithfulness tests against
invented fixtures prove nothing.
MVP is phases 1-3 (T001-T023): the column works on its own. Stories 2-4 cannot make
a previously-correct cell wrong, because Phase 2 defaults every cell to unavailable
rather than blank.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…le VI reasoning /speckit-analyze found two HIGH and seven MEDIUM issues across spec, plan and tasks. All are remediated; task IDs are unchanged and all 43 still match the checklist format. Citation coverage 83% -> 97%; genuinely uncovered requirements 4 -> 0. The two that mattered: - G1: no task verified FR-022/FR-023, the two data-locality requirements. This feature adds a new outbound HTTP path and nothing asserted it carries no credentials and nothing inventory-derived — the exact bound Principle V exists to enforce, and one of the three principles the Review gate names explicitly. Folded into T013, which already builds the captured-request harness, asserting against the full request rather than the URL alone so a header added later cannot slip past. - D1: Principle VI requires a shared-surface function to report unreachability "as an error rather than as a set of undecided verdicts", while FR-029 requires scope retrieval to be non-fatal. Nothing adjudicated it, so the PR gate reviewer would have had to block or wave it through unreasoned. The plan's Principle VI row now argues it: the clause protects verdicts, a scope statement is not one (FR-011), and the distinct `unreachable` and `no-content` causes preserve the "could not ask" vs "asked and got nothing" distinction more finely than raising would. Quantifying FR-046's courtesy delay (A1) exposed a real conflict rather than just vague wording: the 0.4 s measured during research puts a 25-advisory bundle at ~26% added elapsed time and breaches FR-039's one-quarter budget. FR-046 now bounds the delay at 150-250 ms with the arithmetic stated, and FR-043's retry delay references it instead of saying "a short delay". Also: FR-011 gained a test task for ordering/filters/sorts (T023) rather than only an implementation constraint; T039 names test_html_report.py as FR-037's guard and adds a no-external-URL assertion for FR-036; T014 asserts scope_targets does not mutate the caller's devices, a property the Tests gate names; plan.md's "five modules touched" corrected to three modified plus one added; spec.md notes that FR identifiers are grouped thematically, not positionally; four SC/FR citations added for traceability. FR-012 and SC-001 remain uncited deliberately — FR-012 is subsumed by FR-013, whose no-other-alteration test in T006 is the operative check, and SC-001 is an outcome metric validated indirectly by T040/T041. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
….3.0) The bundle report was authoritative about impact and silent about applicability. A row said "5 devices impacted, fixed in 7.4.6" but never what Cisco scoped the advisory to, nor which Cisco products Cisco had explicitly ruled out — so an analyst opened Cisco's page once per advisory, 25 times per bundle review, and the archived report carried no record of the scope the decision rested on. The publication date moves into the Advisory column beside the CVSS score, and the column it vacated becomes "Affected Products", carrying Cisco's own Vulnerable Products and Products Confirmed Not Vulnerable statements in Cisco's words. The third column becomes "Your impacted devices" so Cisco's generic scope cannot be read as a finding about the reader's estate. New shared-surface module caia/csaf.py — the only module that reads Cisco's published advisory documents. It exists rather than extending erp or psirt for reasons stronger than tidiness: erp is deliberately built with no route to supplying an advisory fact, and psirt owns the credentials this request must not carry. Constitution 3.3.0 admitted the source; 3.3.1 named the module. Retrieval follows the csafUrl the advisory API already returns for every record the report holds, so no URL is constructed and no additional advisory-API request is made — verified live at 0 extra requests. It streams each response and stops once the top-level `document` object parses, measured live at 131 KB against 2.53 MB. One retry on transport failure only; sequential with a 200 ms courtesy delay; every failure non-fatal and rendered as one of five named causes, never a blank cell. Faithfulness is enforced rather than intended: exactly two normalisations are permitted, each has its own test, and a further test reconstructs all eight recorded statements from those two rules alone and asserts the output is identical. The worked example matches Cisco's published text character for character. Defects found and fixed while building: - The extract hid the substance of every not-vulnerable statement. Replacing FR-031's vague "long enough to dominate its row" with "always extract" removed the vagueness but started collapsing short statements, showing the boilerplate "Only products listed in the Vulnerable Products section..." and hiding the four products Cisco rules out. Now gated on a stated 900-character threshold, set from the measured distribution so no not-vulnerable statement ever collapses. - The flow tests were making real network calls (7.4s, from milliseconds), which breaks the constitution's no-network test rule. Injected opener. - delay=COURTESY_DELAY bound at import, so adjusting the constant did nothing and the suite slept for real. Resolved at call time, with a test that catches it. - psirt fixture carried a placeholder csafUrl that did not encode the advisory id, which the identity check correctly rejected. Made realistic — as was the placeholder advisoryTitle, which made rendered previews look as though the title line duplicated the identifier. - The publication timestamp broke mid-value across lines. Value now nowrap. Also corrected: 3.4% is the document object-size floor, not what a chunked reader transfers (5.2% measured live). Spec 008's suite never asserted the column headers, so renaming two of them broke nothing; all three are now pinned. 537 tests pass, up from 402, with no credentials and no network. README and docs/index.html updated for the seven-module shared surface. No new dependency. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
The bundle report was authoritative about impact and silent about applicability. A row read "5 devices · 2 affected releases · fixed in 7.4.6" but never said what Cisco scoped the advisory to, nor which Cisco products Cisco had explicitly confirmed are not vulnerable. So an analyst opened Cisco's page once per advisory — 25 times per review of ERP-75736 — and the archived report carried no record of the scope the patch decision rested on.
This adds that context beside the finding:
AdvisoryAdvisory— + publication date beside the CVSS scorePublished— one 19-char timestamp in 12% of the widthAffected Products— Cisco's two scope statementsImpacted productsYour impacted devicesColumn 3's rename is not cosmetic:
Affected Products(Cisco's scope) andImpacted products(your fleet) are near-synonyms for very different things, and the mistake that invites — reading Cisco's generic scope as a finding about your own estate — is worth designing out.Where the data comes from
A new shared-surface module,
caia/csaf.py— the only module that reads Cisco's published advisory documents. It exists rather than extendingerporpsirtfor reasons stronger than tidiness:erpis deliberately built with no route to supplying an advisory fact. Its docstring states both the rule and the enforcement. Putting content retrieval there would dismantle a safeguard to avoid creating a file.psirtowns the API credentials that this request must not carry. Hosting an unauthenticated path there inverts the separation that prohibition creates.Retrieval follows the
csafUrlthe advisory API already returns in every record the report holds, so no URL is constructed and no additional advisory-API request is made. Verified live: 0 extra requests.Measured live against ERP-75736
Each response is streamed and reading stops as soon as the top-level
documentobject parses —raw_decoderaises while the value is incomplete, so the loop is accumulate → attempt → read more. No JSON repair, no brace counting. One advisory came back at revision1.2.0, which makes the provenance record load-bearing rather than decorative.Safety properties
<,>or&in CLI placeholders like<interface_name>. Unescaped, a browser deletes the placeholder and the text after it — so the tests assert the trailing text survives, not merely that the placeholder renders.Constitution
Both amendments are in this branch and were prerequisites, not afterthoughts:
csafin the shared surface, discharging 3.3.0's own follow-up item.Defects found while building
Worth reading, because two changed the design:
Vulnerable Products217–4,783;Products Confirmed Not Vulnerable218–818) so no not-vulnerable statement ever collapses.delay=COURTESY_DELAYbound at import, so adjusting the constant did nothing and the suite slept for real.csafUrlthat didn't encode the advisory id (correctly rejected by the identity check) and anadvisoryTitleof"Title for <id>"that made rendered previews look as though the title duplicated the identifier.published 2026-03-/04T16:00:00reads as two facts.Also corrected: 3.4% is the
documentobject-size floor, not what a chunked reader transfers (5.2% measured live). And spec 008's suite never asserted the column headers at all, so renaming two of them broke nothing — all three are now pinned.Tests
537 pass, up from 402, in 0.4 s, with no credentials and no network access. Earlier during this work the flow tests took 7.4 s because they were genuinely reaching out; that is fixed and guarded.
Principle compliance
urllib,json,re,htmlonly.Authorizationheader, and nothing inventory-derived leaves the machine — not even the family/platform/release triple the advisory API is permitted. Asserted against the full captured request.csafprints nothing, prompts for nothing, writes no file, exits nothing; progress goes through a caller-supplied callback. The plan records why non-fatal scope failure does not breach the error-versus-undecided clause.README.mdanddocs/index.htmlupdated for the seven-module surface, including correctingerp.py's "only module that talks to Cisco's Security Center".VERSION4.2.1 → 4.3.0 (MINOR: additive report change plus a shared-surface addition).Left for the reviewer
The one validation I could not run from here is the print / PDF check — that collapsed statements appear in full when the report is printed.
<details>keeps the text in the document (asserted), but the rendered output is worth one manual look.🤖 Generated with Claude Code