@blamejs/exceptd-skills 0.14.25 → 0.14.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -49,6 +49,7 @@
49
49
  "ai-api",
50
50
  "ai-discovered-cve-triage",
51
51
  "cicd-pipeline-compromise",
52
+ "citation-hygiene",
52
53
  "cloud-iam-incident",
53
54
  "crypto",
54
55
  "crypto-codebase",
@@ -85,6 +85,7 @@
85
85
  "ai-api",
86
86
  "ai-discovered-cve-triage",
87
87
  "cicd-pipeline-compromise",
88
+ "citation-hygiene",
88
89
  "cloud-iam-incident",
89
90
  "containers",
90
91
  "crypto",
@@ -17746,5 +17746,216 @@
17746
17746
  ],
17747
17747
  "_auto_imported": false,
17748
17748
  "_intake_method": "manual-verified-curation"
17749
+ },
17750
+ "CVE-2025-30066": {
17751
+ "name": "tj-actions/changed-files GitHub Action Supply-Chain Compromise",
17752
+ "lesson_date": "2026-05-28",
17753
+ "attack_vector": {
17754
+ "description": "A stolen Personal Access Token repointed the action's mutable release tags (v1..v45.0.7) to a malicious commit (0e58ed8) that dumped CI/CD secrets into publicly readable GitHub Actions workflow logs. ~23,000 repositories referenced the action; any consumer pinning by tag rather than commit SHA pulled the trojaned code on its next run.",
17755
+ "privileges_required": "none — automatic for any workflow that ran the tag-pinned action during the window",
17756
+ "complexity": "low to use once tags were repointed; the access required a leaked maintainer PAT",
17757
+ "ai_factor": "No AI involvement documented in discovery or weaponization. Static base64 Python memory-dump payload."
17758
+ },
17759
+ "defense_chain": {
17760
+ "prevention": {
17761
+ "what_would_have_worked": "Pinning every GitHub Action to a full-length 40-character commit SHA rather than a mutable tag. A SHA reference cannot be silently repointed.",
17762
+ "was_this_required": false,
17763
+ "framework_requiring_it": "OWASP CICD-SEC-3 (recommended, not mandated as default)",
17764
+ "adequacy": "SHA pinning fully prevents tag-repointing, but the documented usage pattern for the action was tag pinning, so the safe configuration was opt-in."
17765
+ },
17766
+ "detection": {
17767
+ "what_would_have_worked": "Egress/behavior monitoring on CI runners (e.g. Harden-Runner) that flags unexpected network calls or secret-shaped output from a build step — the mechanism by which the compromise was first observed.",
17768
+ "was_this_required": false,
17769
+ "framework_requiring_it": null,
17770
+ "adequacy": "Effective but rarely deployed; most pipelines have no runner egress baseline."
17771
+ },
17772
+ "response": {
17773
+ "what_would_have_worked": "Rotate every secret exposed to affected workflows during 2025-03-14/15 and repin to a known-good SHA. GitHub purged the malicious commit and the action was restored at v46.",
17774
+ "was_this_required": true,
17775
+ "framework_requiring_it": "NIST 800-53 IR-4 / SR-11",
17776
+ "adequacy": "Rotation is mandatory but only as good as the org's ability to enumerate which secrets each workflow could read."
17777
+ }
17778
+ },
17779
+ "framework_coverage": {
17780
+ "SLSA-v1.0-Build-L3": {
17781
+ "covered": true,
17782
+ "adequate": false,
17783
+ "gap": "Build provenance does not bind a consumer's tag reference to a specific source revision; a repointed mutable tag substitutes the build inputs silently."
17784
+ },
17785
+ "NIST-800-218-SSDF-PW.4": {
17786
+ "covered": true,
17787
+ "adequate": false,
17788
+ "gap": "Component-reuse controls assume the upstream artifact is immutable; an action tag is mutable with no publisher-side tamper control."
17789
+ },
17790
+ "NIST-800-53-SR-11": {
17791
+ "covered": true,
17792
+ "adequate": false,
17793
+ "gap": "Component-authenticity verification assumes signed/versioned artifacts; unsigned action tags carry no integrity guarantee."
17794
+ },
17795
+ "OWASP-CICD-SEC-3": {
17796
+ "covered": true,
17797
+ "adequate": false,
17798
+ "gap": "Dependency-chain abuse: SHA pinning is the control but tag pinning is the documented default usage."
17799
+ }
17800
+ },
17801
+ "new_control_requirements": [
17802
+ {
17803
+ "id": "NEW-CTRL-111",
17804
+ "name": "ACTION-COMMIT-SHA-PINNING-ENFORCEMENT",
17805
+ "description": "Every third-party CI/CD action (GitHub Actions, GitLab CI includes, etc.) must be referenced by a full-length commit SHA, never a mutable tag or branch. Enforce via policy check in the pipeline linter; a tag/branch reference to a third-party action is a hard fail.",
17806
+ "evidence": "CVE-2025-30066 — tags v1..v45.0.7 were repointed to malicious commit 0e58ed8; only SHA-pinned consumers were unaffected.",
17807
+ "gap_closes": [
17808
+ "SLSA-v1.0-Build-L3",
17809
+ "OWASP-CICD-SEC-3",
17810
+ "NIST-800-53-SR-11"
17811
+ ]
17812
+ }
17813
+ ],
17814
+ "compliance_exposure_score": {
17815
+ "percent_audit_passing_orgs_still_exposed": 85,
17816
+ "basis": "Most pipelines pin actions by tag per the documented usage pattern, and few audit frameworks mandate SHA pinning as a hard control. Secrets leaked to public logs before any audit could react.",
17817
+ "theater_pattern": "supply_chain_first_party_only"
17818
+ },
17819
+ "ai_discovered_zeroday": false,
17820
+ "ai_discovery_source": "human_researcher",
17821
+ "ai_discovery_date": "2025-03-14",
17822
+ "ai_assist_factor": "none"
17823
+ },
17824
+ "CVE-2025-30154": {
17825
+ "name": "reviewdog/action-setup GitHub Action Supply-Chain Compromise",
17826
+ "lesson_date": "2026-05-28",
17827
+ "attack_vector": {
17828
+ "description": "reviewdog/action-setup@v1 was trojaned on 2025-03-11 to dump exposed secrets to GitHub Actions workflow logs. Because five other reviewdog actions invoke action-setup@v1 internally, consumers were affected transitively even when they SHA-pinned the outer reviewdog action. Assessed as the pivot that leaked the PAT later used in the tj-actions compromise (CVE-2025-30066).",
17829
+ "privileges_required": "none — automatic via transitive action inclusion during the window",
17830
+ "complexity": "low to use; defeated consumer-side SHA pinning of the outer action",
17831
+ "ai_factor": "No AI involvement documented."
17832
+ },
17833
+ "defense_chain": {
17834
+ "prevention": {
17835
+ "what_would_have_worked": "Transitive SHA pinning — verifying that an action's OWN internal dependencies are SHA-pinned, not just the action a consumer references directly. Preferring actions that pin their dependencies by SHA.",
17836
+ "was_this_required": false,
17837
+ "framework_requiring_it": null,
17838
+ "adequacy": "Consumer-side SHA pinning is insufficient when the pinned action references a mutable tag internally; the control must extend one tier deeper."
17839
+ },
17840
+ "detection": {
17841
+ "what_would_have_worked": "CI runner egress/behavior monitoring flagging secret-shaped output from a reviewdog step.",
17842
+ "was_this_required": false,
17843
+ "framework_requiring_it": null,
17844
+ "adequacy": "Effective but rarely deployed."
17845
+ },
17846
+ "response": {
17847
+ "what_would_have_worked": "Rotate secrets exposed during 2025-03-11 18:42-20:31 UTC; repin all reviewdog actions to known-good SHAs predating the compromise.",
17848
+ "was_this_required": true,
17849
+ "framework_requiring_it": "NIST 800-53 IR-4 / SR-11",
17850
+ "adequacy": "Mandatory; complicated by the transitive inclusion masking which workflows were actually affected."
17851
+ }
17852
+ },
17853
+ "framework_coverage": {
17854
+ "SLSA-v1.0-Build-L3": {
17855
+ "covered": true,
17856
+ "adequate": false,
17857
+ "gap": "Provenance does not cover transitively-included actions; SHA-pinning the outer action still pulled a tag-referenced malicious inner action."
17858
+ },
17859
+ "NIST-800-53-SR-3": {
17860
+ "covered": true,
17861
+ "adequate": false,
17862
+ "gap": "Supply-chain inventory captures direct dependencies; a second-tier action (action-setup pulled by action-shellcheck) escapes that inventory."
17863
+ },
17864
+ "OWASP-CICD-SEC-3": {
17865
+ "covered": true,
17866
+ "adequate": false,
17867
+ "gap": "Transitive dependency-chain abuse — consumer SHA pinning is necessary but insufficient."
17868
+ }
17869
+ },
17870
+ "new_control_requirements": [
17871
+ {
17872
+ "id": "NEW-CTRL-112",
17873
+ "name": "TRANSITIVE-ACTION-DEPENDENCY-INTEGRITY",
17874
+ "description": "CI/CD action vetting must extend to the actions a referenced action invokes internally. Prefer actions that SHA-pin their own dependencies; where a dependency is tag-referenced, treat the whole chain as unpinned regardless of how the outer action is pinned.",
17875
+ "evidence": "CVE-2025-30154 — action-setup@v1 (tag) was trojaned and reached consumers who SHA-pinned action-shellcheck/staticcheck/ast-grep/typos/composite-template.",
17876
+ "gap_closes": [
17877
+ "SLSA-v1.0-Build-L3",
17878
+ "NIST-800-53-SR-3",
17879
+ "OWASP-CICD-SEC-3"
17880
+ ]
17881
+ }
17882
+ ],
17883
+ "compliance_exposure_score": {
17884
+ "percent_audit_passing_orgs_still_exposed": 88,
17885
+ "basis": "Even organizations that adopted SHA pinning (the recommended control) were exposed because the malicious code was a transitive tag-referenced dependency of an action they had pinned correctly.",
17886
+ "theater_pattern": "supply_chain_first_party_only"
17887
+ },
17888
+ "ai_discovered_zeroday": false,
17889
+ "ai_discovery_source": "human_researcher",
17890
+ "ai_discovery_date": "2025-03-11",
17891
+ "ai_assist_factor": "none"
17892
+ },
17893
+ "CVE-2026-48027": {
17894
+ "name": "Nx Console IDE Extension Supply-Chain Compromise",
17895
+ "lesson_date": "2026-05-28",
17896
+ "attack_vector": {
17897
+ "description": "A malicious Nx Console 18.95.0 was published to the Visual Studio Marketplace (~18 min) and OpenVSX (~36 min) on 2026-05-19. On install/activation it fetched an obfuscated payload that harvested developer credentials from multiple sources on the endpoint. The compromise sits upstream of the CI pipeline — on the developer's machine, where the same secrets a pipeline protects are stored.",
17898
+ "privileges_required": "none beyond install/auto-update of the extension during the window; payload ran with the developer's local privileges",
17899
+ "complexity": "low to use; required publishing under the legitimate publisher identity",
17900
+ "ai_factor": "AI-CLI abuse is not asserted for this specific extension compromise. The Nx ecosystem's earlier August 2025 's1ngularity' npm compromise weaponized installed AI CLI assistants for secret enumeration — a distinct incident noted only as context."
17901
+ },
17902
+ "defense_chain": {
17903
+ "prevention": {
17904
+ "what_would_have_worked": "Consumer-verifiable publisher signatures on IDE marketplace extensions, plus disabling auto-update on security-critical developer hosts and verifying publisher/version before updating.",
17905
+ "was_this_required": false,
17906
+ "framework_requiring_it": null,
17907
+ "adequacy": "VS Code/OpenVSX extensions carry no consumer-verifiable publisher signature; version-and-publisher review is manual and rarely performed."
17908
+ },
17909
+ "detection": {
17910
+ "what_would_have_worked": "Endpoint monitoring flagging an IDE extension host process reading credential stores (Git config, ~/.npmrc, SSH keys, cloud credentials, wallet files) or fetching a second-stage payload after update.",
17911
+ "was_this_required": false,
17912
+ "framework_requiring_it": null,
17913
+ "adequacy": "Developer endpoints are seldom instrumented for IDE-extension behavior."
17914
+ },
17915
+ "response": {
17916
+ "what_would_have_worked": "Upgrade to clean Nx Console 18.100.0; if 18.95.0 was installed on 2026-05-19, treat the host as compromised and rotate all developer credentials.",
17917
+ "was_this_required": true,
17918
+ "framework_requiring_it": "NIST 800-53 IR-4",
17919
+ "adequacy": "Mandatory; exposure window was short but the credential blast radius per host is large."
17920
+ }
17921
+ },
17922
+ "framework_coverage": {
17923
+ "NIST-800-53-SR-11": {
17924
+ "covered": true,
17925
+ "adequate": false,
17926
+ "gap": "Component-authenticity verification does not extend to IDE marketplace extensions, which carry no consumer-verifiable publisher signature."
17927
+ },
17928
+ "NIST-800-218-SSDF-PW.4": {
17929
+ "covered": true,
17930
+ "adequate": false,
17931
+ "gap": "Trusted-component reuse assumes the marketplace artifact matches reviewed source; a malicious version under the legitimate publisher identity defeats that."
17932
+ },
17933
+ "ISO-27001-2022-A.8.8": {
17934
+ "covered": true,
17935
+ "adequate": false,
17936
+ "gap": "Technical-vulnerability management for developer endpoints rarely inventories IDE extensions or their auto-update behavior as a managed surface."
17937
+ }
17938
+ },
17939
+ "new_control_requirements": [
17940
+ {
17941
+ "id": "NEW-CTRL-113",
17942
+ "name": "IDE-EXTENSION-MARKETPLACE-INTEGRITY",
17943
+ "description": "Treat IDE extensions as a managed software supply-chain surface: inventory installed extensions, pin/approve versions for security-critical hosts, disable silent auto-update where feasible, and monitor extension-host processes for credential-store access and unexpected egress.",
17944
+ "evidence": "CVE-2026-48027 — a malicious Nx Console 18.95.0 was live in two marketplaces for minutes and harvested developer credentials on install/update.",
17945
+ "gap_closes": [
17946
+ "NIST-800-53-SR-11",
17947
+ "ISO-27001-2022-A.8.8"
17948
+ ]
17949
+ }
17950
+ ],
17951
+ "compliance_exposure_score": {
17952
+ "percent_audit_passing_orgs_still_exposed": 92,
17953
+ "basis": "Almost no organization manages developer IDE extensions as a vetted software surface; marketplace publication under a legitimate identity bypasses endpoint controls and auto-update delivers the malicious version before review.",
17954
+ "theater_pattern": "endpoint_dev_tooling_unmanaged"
17955
+ },
17956
+ "ai_discovered_zeroday": false,
17957
+ "ai_discovery_source": "human_researcher",
17958
+ "ai_discovery_date": "2026-05-19",
17959
+ "ai_assist_factor": "none"
17749
17960
  }
17750
17961
  }
@@ -204,35 +204,47 @@ function titleAcronym(realTitle) {
204
204
  }
205
205
 
206
206
  /**
207
- * Decide whether an adjacent text fragment makes a TITLE CLAIM that
208
- * conflicts with the real index title. Conservative by design the cost
209
- * of a false positive (telling an author their correct citation is wrong)
210
- * is high, so the bar to flag a mismatch is deliberately strict:
211
- * - the cited RFC number must be in the index (checked by the caller),
212
- * - the adjacent text must carry at least THREE meaningful tokens a
213
- * bare nickname / abbreviation ("TLS 1.3", "(HTTP)") reduces below
214
- * this and is treated as no-title-claim, never a mismatch,
215
- * - if the adjacent tokens contain the title's acronym, it is the same
216
- * document (TLS for Transport Layer Security); not a mismatch,
217
- * - only ZERO overlap between the meaningful adjacent tokens and the
218
- * real-title tokens flags a mismatch. Any shared content word means
219
- * the author is describing the right document (paraphrase) — demote.
207
+ * Decide whether an EXPLICITLY STATED title conflicts with the real index
208
+ * title. The stated title is extracted by the caller (see statedTitleAfter):
209
+ * only a title introduced immediately after the RFC number by a delimiter —
210
+ * `RFC N: The Title`, `RFC N "The Title"`, `RFC N (The Title)` — counts.
211
+ *
212
+ * This is deliberately strict because the dominant real-world pattern is a
213
+ * mechanism citation "CRLF line endings per RFC 5322", "renders values per
214
+ * RFC 8785", "ETag repeated per RFC 7232 §4.1" — where the prose describes
215
+ * what the code does *per* the RFC using vocabulary that never overlaps the
216
+ * RFC's formal title. Comparing that prose against the title produced a
217
+ * false "mismatch" on correct citations. Such references state no title and
218
+ * are filtered out before this function is reached.
219
+ *
220
+ * - at least TWO meaningful tokens in the stated title (a bare acronym /
221
+ * nickname is not a title),
222
+ * - the title's acronym appearing in the stated title is the same document
223
+ * (TLS for Transport Layer Security); not a mismatch,
224
+ * - only ZERO overlap between stated-title tokens and real-title tokens
225
+ * flags a mismatch; any shared content word is a paraphrase — demote.
220
226
  * Returns "mismatch" | "match" | "no-title-claim".
221
227
  */
222
- function classifyRfcTitle(adjacentText, realTitle) {
223
- const adjTokens = titleTokens(adjacentText);
224
- // Require a substantive title claim. Fewer than three content tokens is
225
- // a nickname / abbreviation, not a stated title — stay conservative.
226
- if (adjTokens.size < 3) return "no-title-claim";
228
+ function classifyRfcTitle(statedTitle, realTitle) {
229
+ const adjTokens = titleTokens(statedTitle);
230
+ if (adjTokens.size < 2) return "no-title-claim";
227
231
  const realTokens = titleTokens(realTitle);
228
232
  if (realTokens.size === 0) return "no-title-claim";
229
- // Acronym recognition: "tls" in the adjacent text matches "Transport
233
+ // Acronym recognition: "tls" in the stated title matches "Transport
230
234
  // Layer Security". Same document, not a wrong title.
231
235
  const acronym = titleAcronym(realTitle);
232
236
  if (acronym.length >= 2 && adjTokens.has(acronym)) return "match";
233
237
  let overlap = 0;
234
238
  for (const t of adjTokens) {
235
- if (realTokens.has(t)) overlap++;
239
+ if (realTokens.has(t)) { overlap++; continue; }
240
+ // Nickname / short-name recognition: a stated token that contains (or is
241
+ // contained by) a real-title token of length >= 4 is the same document
242
+ // under a common name — "IMAP4rev2" carries the real token "imap"
243
+ // ("...Access Protocol (IMAP)..."). Avoids false mismatches on the way
244
+ // developers actually cite RFCs by their well-known short names.
245
+ for (const rt of realTokens) {
246
+ if (rt.length >= 4 && (t.includes(rt) || rt.includes(t))) { overlap++; break; }
247
+ }
236
248
  }
237
249
  // Any shared content word -> the author is describing the right
238
250
  // document. Only a stated title with ZERO overlap is a conflicting
@@ -249,6 +261,29 @@ function lineAround(content, index) {
249
261
  return content.slice(start, end);
250
262
  }
251
263
 
264
+ // Extract a title EXPLICITLY QUOTED immediately after the RFC number on the
265
+ // same line:
266
+ // RFC N "The Title" RFC N: "The Title" RFC N ("The Title")
267
+ // A quoted string is the only unambiguous title claim. Everything else states
268
+ // no title and returns null:
269
+ // - free prose ("RFC 6455 wire layer"), a section pointer ("RFC 5322 §2.3"),
270
+ // and "X per RFC N" mechanism attributions describe usage, not the title;
271
+ // - bare nicknames ("RFC 9051 (IMAP4rev2)") are common short names;
272
+ // - and crucially, an RFC-number-shaped token inside CODE (`envelope.rfc822`
273
+ // matches "RFC 822"; `RFC 3339:` ahead of an object literal) is followed
274
+ // by code punctuation, never a quoted title — so comparing a code fragment
275
+ // against a formal title can no longer produce a phantom mismatch.
276
+ // The opening quote must be SEPARATED from the number by whitespace or a
277
+ // `:` / `(` introducer. A quote touching the last digit (`"…RFC 3339"`) is the
278
+ // CLOSING quote of a string that happens to end with the citation, not the
279
+ // opening quote of a title — without this guard the following code was
280
+ // captured as a phantom "title". The closing quote bounds the title; straight
281
+ // and typographic quotes are accepted.
282
+ function statedTitleAfter(after) {
283
+ const m = after.match(/^(?:\s*[:(]\s*|\s+)["“]([^"”\n]{3,100})["”]/);
284
+ return m ? m[1].trim() : null;
285
+ }
286
+
252
287
  function collect({ cwd = process.cwd() } = {}) {
253
288
  const errors = [];
254
289
  const startTime = Date.now();
@@ -339,7 +374,10 @@ function collect({ cwd = process.cwd() } = {}) {
339
374
  const line = lineAround(content, m.index);
340
375
  const rfcLineNo = lineFromOffset(content, m.index);
341
376
  if (rfcTitles.has(num)) {
342
- const verdict = classifyRfcTitle(line, rfcTitles.get(num));
377
+ const lineStart = content.lastIndexOf("\n", m.index) + 1;
378
+ const after = line.slice((m.index - lineStart) + m[0].length);
379
+ const stated = statedTitleAfter(after);
380
+ const verdict = stated ? classifyRfcTitle(stated, rfcTitles.get(num)) : "no-title-claim";
343
381
  if (verdict === "mismatch" && !illustrative) {
344
382
  hits["rfc-number-title-mismatch"].push({
345
383
  file: f.rel,