@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.
- package/CHANGELOG.md +14 -0
- package/data/_indexes/_meta.json +9 -9
- package/data/_indexes/activity-feed.json +3 -3
- package/data/_indexes/catalog-summaries.json +7 -7
- package/data/_indexes/chains.json +1125 -0
- package/data/_indexes/frequency.json +2 -0
- package/data/atlas-ttps.json +29 -17
- package/data/attack-techniques.json +43 -36
- package/data/cve-catalog.json +321 -2
- package/data/cwe-catalog.json +22 -19
- package/data/framework-control-gaps.json +197 -116
- package/data/playbooks/framework.json +1 -0
- package/data/playbooks/sbom.json +1 -0
- package/data/zeroday-lessons.json +211 -0
- package/lib/collectors/citation-hygiene.js +59 -21
- package/manifest.json +44 -44
- package/package.json +1 -1
- package/sbom.cdx.json +30 -30
package/data/playbooks/sbom.json
CHANGED
|
@@ -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
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
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(
|
|
223
|
-
const adjTokens = titleTokens(
|
|
224
|
-
|
|
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
|
|
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
|
|
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,
|