@clear-capabilities/agentic-security-scanner 0.145.0 → 0.147.0

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.
Files changed (162) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/bin/agentic-security.js +3508 -69
  3. package/dist/1122.index.js +702 -0
  4. package/dist/{301.index.js → 1301.index.js} +2 -2
  5. package/dist/1379.index.js +591 -0
  6. package/dist/{444.index.js → 1444.index.js} +2 -2
  7. package/dist/{660.index.js → 1660.index.js} +2 -2
  8. package/dist/{700.index.js → 1700.index.js} +2 -2
  9. package/dist/{905.index.js → 1905.index.js} +3 -3
  10. package/dist/{920.index.js → 1920.index.js} +3 -3
  11. package/dist/{238.index.js → 2238.index.js} +3 -3
  12. package/dist/{271.index.js → 2271.index.js} +2 -2
  13. package/dist/{985.index.js → 2376.index.js} +1260 -340
  14. package/dist/2432.index.js +793 -0
  15. package/dist/2659.index.js +93 -0
  16. package/dist/{826.index.js → 2826.index.js} +2 -2
  17. package/dist/{830.index.js → 2830.index.js} +2 -2
  18. package/dist/2923.index.js +298 -0
  19. package/dist/{1.index.js → 3001.index.js} +5 -5
  20. package/dist/{117.index.js → 3117.index.js} +3 -3
  21. package/dist/3180.index.js +307 -0
  22. package/dist/3276.index.js +117 -0
  23. package/dist/{415.index.js → 3415.index.js} +2 -2
  24. package/dist/{499.index.js → 3499.index.js} +2 -2
  25. package/dist/3518.index.js +450 -0
  26. package/dist/{526.index.js → 3526.index.js} +5 -5
  27. package/dist/{736.index.js → 3736.index.js} +4 -4
  28. package/dist/{839.index.js → 3839.index.js} +4 -4
  29. package/dist/{113.index.js → 4113.index.js} +5 -5
  30. package/dist/{265.index.js → 4265.index.js} +2 -2
  31. package/dist/{384.index.js → 4384.index.js} +3 -3
  32. package/dist/4547.index.js +268 -0
  33. package/dist/4863.index.js +422 -0
  34. package/dist/{970.index.js → 4970.index.js} +2 -2
  35. package/dist/5051.index.js +770 -0
  36. package/dist/{144.index.js → 5144.index.js} +5 -5
  37. package/dist/{333.index.js → 5333.index.js} +3 -3
  38. package/dist/5343.index.js +185 -0
  39. package/dist/5350.index.js +866 -0
  40. package/dist/5561.index.js +436 -0
  41. package/dist/{637.index.js → 5637.index.js} +3 -3
  42. package/dist/{449.index.js → 5830.index.js} +2 -2
  43. package/dist/6626.index.js +532 -0
  44. package/dist/6662.index.js +297 -0
  45. package/dist/{675.index.js → 6675.index.js} +5 -5
  46. package/dist/{730.index.js → 6730.index.js} +6 -6
  47. package/dist/6829.index.js +225 -0
  48. package/dist/6944.index.js +130 -0
  49. package/dist/{178.index.js → 7178.index.js} +3 -3
  50. package/dist/{227.index.js → 7227.index.js} +2 -2
  51. package/dist/7310.index.js +520 -0
  52. package/dist/{552.index.js → 7552.index.js} +4 -4
  53. package/dist/7709.index.js +78 -0
  54. package/dist/8218.index.js +160 -0
  55. package/dist/{476.index.js → 8476.index.js} +4 -4
  56. package/dist/{513.index.js → 8513.index.js} +5 -5
  57. package/dist/{520.index.js → 8520.index.js} +2 -2
  58. package/dist/{718.index.js → 8718.index.js} +2 -2
  59. package/dist/{752.index.js → 8752.index.js} +2 -2
  60. package/dist/8846.index.js +100 -0
  61. package/dist/{435.index.js → 9091.index.js} +560 -189
  62. package/dist/{207.index.js → 9207.index.js} +2 -2
  63. package/dist/{220.index.js → 9220.index.js} +2 -2
  64. package/dist/9390.index.js +163 -0
  65. package/dist/{503.index.js → 9503.index.js} +2 -2
  66. package/dist/{801.index.js → 9801.index.js} +2 -2
  67. package/dist/{824.index.js → 9824.index.js} +2 -2
  68. package/dist/agentic-security.mjs +16 -16
  69. package/dist/agentic-security.mjs.sha256 +1 -1
  70. package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
  71. package/package.json +12 -5
  72. package/src/dataflow/CLAUDE.md +1 -1
  73. package/src/dataflow/catalog.js +42 -0
  74. package/src/dataflow/orm-write-catalog.js +175 -0
  75. package/src/engine.js +92 -1
  76. package/src/ir/CLAUDE.md +1 -0
  77. package/src/ir/chrome-probe.mjs +150 -0
  78. package/src/ir/parser-js.js +94 -7
  79. package/src/lineage/CLAUDE.md +1203 -0
  80. package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
  81. package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
  82. package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
  83. package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
  84. package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
  85. package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
  86. package/src/lineage/DESIGN_REGISTRIES.md +880 -0
  87. package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
  88. package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
  89. package/src/lineage/classification.js +56 -0
  90. package/src/lineage/coverage.js +658 -0
  91. package/src/lineage/cross-repo-link.js +107 -0
  92. package/src/lineage/dataflow-graph.schema.json +184 -0
  93. package/src/lineage/decision-story.js +206 -0
  94. package/src/lineage/drift-policy.js +279 -0
  95. package/src/lineage/driver.js +135 -0
  96. package/src/lineage/engine.js +992 -0
  97. package/src/lineage/export-briefing.js +628 -0
  98. package/src/lineage/export-csv.js +62 -0
  99. package/src/lineage/export-json.js +238 -0
  100. package/src/lineage/export-privacy.js +258 -0
  101. package/src/lineage/federation-loader.js +111 -0
  102. package/src/lineage/field-identity.js +78 -0
  103. package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
  104. package/src/lineage/fixtures/flagship-graph.json +1453 -0
  105. package/src/lineage/flow-grade.js +221 -0
  106. package/src/lineage/governance-edit.js +169 -0
  107. package/src/lineage/graph-builder.js +1114 -0
  108. package/src/lineage/graph-diff.js +431 -0
  109. package/src/lineage/graph-snapshot.js +180 -0
  110. package/src/lineage/handling-analyzer.js +168 -0
  111. package/src/lineage/ids.js +349 -0
  112. package/src/lineage/impact-assessment.js +76 -0
  113. package/src/lineage/impact-engine.js +268 -0
  114. package/src/lineage/index.js +281 -0
  115. package/src/lineage/language-coverage-tiers.js +58 -0
  116. package/src/lineage/obligation-mapping.js +126 -0
  117. package/src/lineage/obligation-predicates.js +235 -0
  118. package/src/lineage/observation-adapters.js +282 -0
  119. package/src/lineage/observation-correlation.js +622 -0
  120. package/src/lineage/observation-store.js +497 -0
  121. package/src/lineage/path-query.js +410 -0
  122. package/src/lineage/path-store.js +400 -0
  123. package/src/lineage/protection.js +53 -0
  124. package/src/lineage/recipient-profile.js +192 -0
  125. package/src/lineage/recipient-registry.js +394 -0
  126. package/src/lineage/redact-graph.js +224 -0
  127. package/src/lineage/remediation.js +417 -0
  128. package/src/lineage/resolve-destination.js +91 -0
  129. package/src/lineage/runtime-observation.js +464 -0
  130. package/src/lineage/scenario-diff.js +84 -0
  131. package/src/lineage/scenario-engine.js +251 -0
  132. package/src/lineage/scenario.js +101 -0
  133. package/src/lineage/schema.js +167 -0
  134. package/src/lineage/sink-registry.js +427 -0
  135. package/src/lineage/source-registry.js +357 -0
  136. package/src/lineage/source-seeding.js +212 -0
  137. package/src/lineage/summaries.js +590 -0
  138. package/src/lineage/transform-catalog.js +397 -0
  139. package/src/lineage/transit-protection.js +150 -0
  140. package/src/lineage/validate.js +285 -0
  141. package/src/mcp/CLAUDE.md +7 -1
  142. package/src/mcp/dataflow-tools.js +160 -0
  143. package/src/mcp/server.js +1 -1
  144. package/src/mcp/tools.js +2 -1
  145. package/src/pipeline/scan-health.js +19 -1
  146. package/src/posture/CLAUDE.md +19 -0
  147. package/src/posture/artifact-registry.js +52 -0
  148. package/src/posture/auditor-walkthrough.js +76 -0
  149. package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
  150. package/src/posture/obligation-evidence-pack.js +202 -0
  151. package/src/posture/remediation-ledger.js +337 -0
  152. package/src/server/CLAUDE.md +47 -0
  153. package/src/server/graph-loader.js +141 -0
  154. package/src/server/http-server.js +325 -0
  155. package/src/server/routes.js +129 -0
  156. package/src/server/security.js +111 -0
  157. package/src/server/static-assets.js +139 -0
  158. package/dist/11.index.js +0 -353
  159. package/dist/259.index.js +0 -975
  160. package/dist/317.index.js +0 -300
  161. package/dist/609.index.js +0 -741
  162. package/dist/838.index.js +0 -152
@@ -0,0 +1,139 @@
1
+ // static-assets.js — Milestone 3, sub-project Wire.
2
+ //
3
+ // Confines the `explore` server's new same-origin static-asset surface to an
4
+ // explicit ALLOWLIST of `frontend/`'s own real, servable files. This is the
5
+ // first place scanner/src/server/ ever reads arbitrary requested paths off
6
+ // disk — S1's five JSON endpoints never touched the filesystem beyond the
7
+ // one already-loaded, already-verified graph — so path confinement here is
8
+ // genuinely load-bearing, not decorative (see the M3-Wire scoping doc's
9
+ // Decision 2, and the threat-model doc's T4 entry).
10
+ //
11
+ // `resolveStaticAsset()` is a PURE function: no fs/http access anywhere in
12
+ // this file, fully unit-testable in isolation. http-server.js is the only
13
+ // caller that actually reads a resolved file off disk.
14
+
15
+ import * as path from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ // Located the SAME way scanner/src/mcp/server.js locates files relative to
19
+ // its own module (path.dirname(fileURLToPath(import.meta.url))) — the real,
20
+ // existing precedent for this pattern in this codebase, not a new one.
21
+ // scanner/src/server/ -> ../../../frontend, computed (not guessed) and
22
+ // confirmed to resolve to the real frontend/ directory.
23
+ const _here = path.dirname(fileURLToPath(import.meta.url));
24
+ export const FRONTEND_ROOT = path.resolve(_here, '..', '..', '..', 'frontend');
25
+
26
+ export const CONTENT_TYPE_MAP = Object.freeze({
27
+ '.html': 'text/html; charset=utf-8',
28
+ '.js': 'text/javascript; charset=utf-8',
29
+ '.css': 'text/css; charset=utf-8',
30
+ });
31
+
32
+ // Same-origin-permitting CSP for the static/HTML surface — deliberately
33
+ // DIFFERENT from security.js's CSP_HEADER_VALUE ("default-src 'none'"),
34
+ // which stays correct for the JSON-only /api/v1/* routes (unchanged). This
35
+ // one needs to let the page load its OWN same-origin script/style and make
36
+ // its OWN same-origin fetch() calls, while still denying everything
37
+ // cross-origin — no remote scripts/fonts/analytics, matching
38
+ // frontend/README.md's own "no CDN scripts, remote fonts, analytics" rule.
39
+ //
40
+ // Grepped across the whole frontend/ tree for any inline <script>/
41
+ // style="..." attribute/<style> tag beyond the one index.html inline
42
+ // <script type="module"> block this increment moves into main.js (see
43
+ // item 5 of the plan): none found (confirmed this session, 2026-09-01).
44
+ // So a strict script-src 'self'/style-src 'self' needs no 'unsafe-inline'
45
+ // exception — do not add one without re-running that grep.
46
+ export const STATIC_CSP_HEADER_VALUE =
47
+ "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'";
48
+
49
+ // Deliberately restrictive character class (no backslash, no space, no
50
+ // percent sign, no null) — every real filename in frontend/'s inventory is
51
+ // plain ASCII with letters/digits/hyphen/underscore/dot/slash, and keeping
52
+ // the allowlist regex this narrow means an unexpected character in a
53
+ // request path fails allowlisting by default rather than being silently
54
+ // accepted and handed to a platform-specific path.join() later.
55
+ const SRC_JS_RE = /^src\/[A-Za-z0-9/_.-]+\.js$/;
56
+ const STYLES_CSS_RE = /^styles\/[A-Za-z0-9_.-]+\.css$/; // top-level only under styles/, no nested subdirectories — matches the real, current inventory
57
+
58
+ /**
59
+ * Resolves a request pathname (e.g. `/`, `/src/app.js`, `/styles/tokens.css`)
60
+ * against `frontend/`'s explicit servable-file allowlist. Returns
61
+ * `{ok:true, relativePath, contentType}` or `{ok:false, reason}`.
62
+ *
63
+ * Deliberately an ALLOWLIST, not merely a `../`-traversal guard: `test/`,
64
+ * `scripts/`, `package.json`, `README.md`, `CLAUDE.md`, and `.gitignore` are
65
+ * all real files that exist under FRONTEND_ROOT on disk and must NEVER be
66
+ * served — a naive "stays inside frontend/" confinement check would still
67
+ * happily serve every one of them.
68
+ */
69
+ export function resolveStaticAsset(requestPath) {
70
+ if (typeof requestPath !== 'string' || requestPath.length === 0) {
71
+ return { ok: false, reason: 'invalid-path' };
72
+ }
73
+ if (requestPath.includes('\0')) {
74
+ return { ok: false, reason: 'null-byte' };
75
+ }
76
+ // Absolute-URL-looking input: a scheme (`http://...`) or a
77
+ // protocol-relative `//host/...` — neither is a valid same-origin
78
+ // pathname this server should ever try to resolve.
79
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(requestPath) || requestPath.startsWith('//')) {
80
+ return { ok: false, reason: 'absolute-url' };
81
+ }
82
+ if (!requestPath.startsWith('/')) {
83
+ return { ok: false, reason: 'invalid-path' };
84
+ }
85
+
86
+ let decoded;
87
+ try {
88
+ decoded = decodeURIComponent(requestPath);
89
+ } catch {
90
+ return { ok: false, reason: 'malformed-encoding' };
91
+ }
92
+ if (decoded.includes('\0')) {
93
+ // Catches a %00-encoded null byte the raw-string check above can't see.
94
+ return { ok: false, reason: 'null-byte' };
95
+ }
96
+
97
+ if (decoded === '/' || decoded === '') {
98
+ return { ok: true, relativePath: 'index.html', contentType: CONTENT_TYPE_MAP['.html'] };
99
+ }
100
+ if (decoded.startsWith('//')) {
101
+ return { ok: false, reason: 'absolute-url' };
102
+ }
103
+
104
+ const candidate = decoded.slice(1); // strip the single leading '/'
105
+
106
+ // Normalize using POSIX rules regardless of host OS — this is a URL path,
107
+ // never a platform filesystem path, and must be treated as forward-slash
108
+ // segments even on a host where path.normalize would use backslashes.
109
+ const normalized = path.posix.normalize(candidate);
110
+
111
+ // Re-check for a traversal escape AFTER normalization — never trust
112
+ // normalize() alone to have removed a traversal attempt (a classic
113
+ // traversal-bypass class): explicitly reject anything that still IS, or
114
+ // still CONTAINS, a `..` segment, or that normalized to an absolute path.
115
+ if (
116
+ normalized === '..' ||
117
+ normalized === '.' ||
118
+ normalized.startsWith('../') ||
119
+ normalized.includes('/../') ||
120
+ normalized.endsWith('/..') ||
121
+ normalized.startsWith('/')
122
+ ) {
123
+ return { ok: false, reason: 'path-traversal' };
124
+ }
125
+
126
+ // --- The actual ALLOWLIST (default: deny) ---
127
+
128
+ if (normalized === 'index.html') {
129
+ return { ok: true, relativePath: normalized, contentType: CONTENT_TYPE_MAP['.html'] };
130
+ }
131
+ if (SRC_JS_RE.test(normalized)) {
132
+ return { ok: true, relativePath: normalized, contentType: CONTENT_TYPE_MAP['.js'] };
133
+ }
134
+ if (STYLES_CSS_RE.test(normalized)) {
135
+ return { ok: true, relativePath: normalized, contentType: CONTENT_TYPE_MAP['.css'] };
136
+ }
137
+
138
+ return { ok: false, reason: 'not-allowlisted' };
139
+ }
package/dist/11.index.js DELETED
@@ -1,353 +0,0 @@
1
- export const id = 11;
2
- export const ids = [11];
3
- export const modules = {
4
-
5
- /***/ 11:
6
- /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
-
8
- // ESM COMPAT FLAG
9
- __webpack_require__.r(__webpack_exports__);
10
-
11
- // EXPORTS
12
- __webpack_require__.d(__webpack_exports__, {
13
- runProjectLinter: () => (/* binding */ runProjectLinter),
14
- verifyFix: () => (/* binding */ verifyFix),
15
- verifyPatch: () => (/* binding */ verifyPatch)
16
- });
17
-
18
- // EXTERNAL MODULE: external "node:child_process"
19
- var external_node_child_process_ = __webpack_require__(1421);
20
- // EXTERNAL MODULE: external "node:fs"
21
- var external_node_fs_ = __webpack_require__(3024);
22
- // EXTERNAL MODULE: external "node:path"
23
- var external_node_path_ = __webpack_require__(6760);
24
- // EXTERNAL MODULE: ./src/engine.js + 592 modules
25
- var engine = __webpack_require__(9408);
26
- ;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
27
- // Deterministic honesty gates on fix / finding output (#7).
28
- //
29
- // The project's verification discipline (scanner/CLAUDE.md) exists because
30
- // several releases shipped broken or false because work was reported as done
31
- // without confirming the artifact changed. Two of those failure modes are
32
- // *textual* — they live in the prose an agent emits alongside a fix — and can
33
- // be caught deterministically, with no LLM and no network:
34
- //
35
- // 1. Hand-wave residual-risk prose. "The input is adequately handled",
36
- // "future work", "tbd", "later" — vague assurances that claim safety
37
- // without naming a concrete remaining vector. A residual you can't name
38
- // is a residual you're guessing about; reject the guess.
39
- //
40
- // 2. An unbacked "this is a false positive / provably safe" verdict. Marking
41
- // a finding safe is a coverage *reduction* — it must cite a `file:line`
42
- // that shows why, exactly like the rules-override gate refuses to silently
43
- // shrink coverage.
44
- //
45
- // Plus a conservative fix-tier classifier so a partial remediation can never be
46
- // labelled FULL: any workaround-only signal (rate-limit, docs, log-without-
47
- // reject) is WORKAROUND; anything short of (sink signature changed + all callers
48
- // routed + a discriminating test) is at most MITIGATION; only the full set with
49
- // no partial-sanitization caveat earns FULL.
50
- //
51
- // Pure functions, no side effects, no throwing — safe to call from a command,
52
- // a hook, or the MCP verify_fix path.
53
-
54
- // Vague-assurance phrases that a real residual must never hide behind. Matched
55
- // case-insensitively with word boundaries so "later" doesn't trip on
56
- // "collateral" and "tbd" doesn't trip on a longer token.
57
- const BANNED_RESIDUAL_PHRASES = Object.freeze([
58
- 'adequately handled',
59
- 'adequately handles',
60
- 'properly validated',
61
- 'properly handled',
62
- 'handled properly',
63
- 'handled safely',
64
- 'future work',
65
- 'more work needed',
66
- 'to be done',
67
- 'tbd',
68
- 'later',
69
- ]);
70
-
71
- // A citation shaped like `file:line` — one or more non-space, non-colon chars,
72
- // a colon, then digits. Unanchored: it need only appear somewhere in the item.
73
- const CITATION_RE = /[^\s:]+:\d+/;
74
-
75
- // Verdicts that assert the finding is not real and therefore demand a citation.
76
- // Compared after normalizing separators (`_`/space → `-`) and lowercasing, so
77
- // FALSE_POSITIVE, false-positive, and "provably safe" all land here.
78
- const FP_VERDICTS = Object.freeze(new Set(['false-positive', 'provably-safe', 'safe']));
79
-
80
- function _escapeRe(s) {
81
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
82
- }
83
-
84
- /**
85
- * Reject vague-assurance / hand-wave residual-risk prose.
86
- *
87
- * An empty or whitespace-only residual is ok — there is no residual to lie
88
- * about. A non-empty residual is rejected when it contains any banned phrase;
89
- * each match yields one violation naming the offending phrase.
90
- *
91
- * @param {string} residualText
92
- * @returns {{ ok: boolean, violations: string[] }}
93
- */
94
- function checkResidualHonesty(residualText) {
95
- const text = typeof residualText === 'string' ? residualText : '';
96
- if (text.trim() === '') return { ok: true, violations: [] };
97
-
98
- const violations = [];
99
- for (const phrase of BANNED_RESIDUAL_PHRASES) {
100
- const re = new RegExp(`\\b${_escapeRe(phrase)}\\b`, 'i');
101
- if (re.test(text)) {
102
- violations.push(`vague-assurance phrase: "${phrase}"`);
103
- }
104
- }
105
- return { ok: violations.length === 0, violations };
106
- }
107
-
108
- function _isCitation(item) {
109
- if (typeof item === 'string') return CITATION_RE.test(item);
110
- if (item && typeof item === 'object' && typeof item.location === 'string') {
111
- return CITATION_RE.test(item.location);
112
- }
113
- return false;
114
- }
115
-
116
- function _normalizeVerdict(verdict) {
117
- return String(verdict).trim().toLowerCase().replace(/[_\s]+/g, '-');
118
- }
119
-
120
- /**
121
- * Require a file:line citation behind a "this is not real" verdict.
122
- *
123
- * For a false-positive / provably-safe / safe verdict (case-insensitive; also
124
- * accepts FALSE_POSITIVE), at least one evidence item must be a `file:line`
125
- * citation — either a string matching /[^\s:]+:\d+/ or an object
126
- * `{ location: "file:line" }`. Any other verdict passes unconditionally.
127
- *
128
- * @param {string} verdict
129
- * @param {Array|string|object} evidence
130
- * @returns {{ ok: boolean, violations: string[] }}
131
- */
132
- function requireCitedEvidence(verdict, evidence) {
133
- if (typeof verdict !== 'string' || !FP_VERDICTS.has(_normalizeVerdict(verdict))) {
134
- return { ok: true, violations: [] };
135
- }
136
- const items = Array.isArray(evidence)
137
- ? evidence
138
- : evidence == null
139
- ? []
140
- : [evidence];
141
- if (items.some(_isCitation)) return { ok: true, violations: [] };
142
- return {
143
- ok: false,
144
- violations: ['false-positive/safe verdict requires a file:line citation'],
145
- };
146
- }
147
-
148
- /**
149
- * Classify a fix into FULL | MITIGATION | WORKAROUND, conservative-first.
150
- *
151
- * @param {object} signals
152
- * @param {boolean} signals.sinkSignatureChanged
153
- * @param {boolean} signals.allCallersRouted
154
- * @param {boolean} signals.testDiscriminates - a test that fails pre-fix, passes post-fix
155
- * @param {boolean} [signals.rateLimitOnly]
156
- * @param {boolean} [signals.docsOnly]
157
- * @param {boolean} [signals.logOnlyNoReject]
158
- * @param {boolean} [signals.partialSanitization]
159
- * @returns {'FULL'|'MITIGATION'|'WORKAROUND'}
160
- */
161
- function computeFixTier(signals) {
162
- const s = signals && typeof signals === 'object' ? signals : {};
163
- if (s.rateLimitOnly || s.docsOnly || s.logOnlyNoReject) return 'WORKAROUND';
164
- const complete = s.sinkSignatureChanged && s.allCallersRouted && s.testDiscriminates;
165
- if (s.partialSanitization || !complete) return 'MITIGATION';
166
- return 'FULL';
167
- }
168
-
169
- /**
170
- * Compose the three gates for a single fix's output.
171
- *
172
- * ok = residual-honesty ok AND evidence-citation ok, further constrained by the
173
- * tier/residual consistency invariant:
174
- * - a FULL tier must NOT carry a residual (a full fix has nothing left);
175
- * - a non-FULL tier MUST document a residual (say what's still open).
176
- *
177
- * @param {{ residual?: string, verdict?: string, evidence?: any, signals?: object }} input
178
- * @returns {{ ok: boolean, tier: string, violations: string[] }}
179
- */
180
- function gateFixOutput({ residual, verdict, evidence, signals } = {}) {
181
- const tier = computeFixTier(signals);
182
- const residualCheck = checkResidualHonesty(residual);
183
- const evidenceCheck = requireCitedEvidence(verdict, evidence);
184
-
185
- const violations = [...residualCheck.violations, ...evidenceCheck.violations];
186
- let ok = residualCheck.ok && evidenceCheck.ok;
187
-
188
- const residualEmpty = typeof residual !== 'string' || residual.trim() === '';
189
- if (tier === 'FULL' && !residualEmpty) {
190
- violations.push('FULL tier cannot carry a residual');
191
- ok = false;
192
- }
193
- if (tier !== 'FULL' && residualEmpty) {
194
- violations.push('non-FULL tier must document a residual');
195
- ok = false;
196
- }
197
-
198
- return { ok, tier, violations };
199
- }
200
-
201
- const _internals = Object.freeze({ BANNED_RESIDUAL_PHRASES, CITATION_RE, FP_VERDICTS });
202
-
203
- ;// CONCATENATED MODULE: ./src/posture/fix-verify.js
204
- // Closed-loop /fix verification (Sentinel-parity FR-L4-4, FR-L4-5).
205
- //
206
- // Given a candidate patch (the new file content + the finding stableId being
207
- // fixed), verify it:
208
- //
209
- // 1. The original finding's stableId no longer fires on the patched file.
210
- // 2. No new findings at severity ≥ medium were introduced by the patch.
211
- // 3. The project's existing linter (when present) passes on the patched file.
212
- //
213
- // If any of those fail, the caller is expected to NOT apply the patch and
214
- // instead surface a "fix plan" — a numbered list of steps the engineer can
215
- // follow — rather than dump a broken patch on the user.
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
- const SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
224
-
225
- // Run a focused re-scan over just the patched file(s) using the in-memory
226
- // engine. No filesystem write needed — we hand the new content in via the
227
- // fileContents map.
228
- async function verifyPatch({
229
- scanRoot,
230
- originalFindingStableId,
231
- files, // { [relPath]: newContent }
232
- depFileContents = {},
233
- } = {}) {
234
- if (!files || typeof files !== 'object') return { ok: false, reason: 'no-files-provided' };
235
- const fileContents = { ...files };
236
- let scan;
237
- try {
238
- scan = await (0,engine/* runFullScan */.wW)({ fileContents, depFileContents, scanRoot }, () => {});
239
- } catch (e) {
240
- return { ok: false, reason: 'rescan-failed', error: e.message };
241
- }
242
- const findings = (scan && scan.findings) || [];
243
- const stillHasOriginal = !!originalFindingStableId &&
244
- findings.some(f => f.stableId === originalFindingStableId);
245
- if (stillHasOriginal) {
246
- return { ok: false, reason: 'original-finding-still-present', stableId: originalFindingStableId };
247
- }
248
- const introducedHighOrAbove = findings.filter(f =>
249
- (SEVERITY_RANK[f.severity] ?? 9) <= SEVERITY_RANK.medium);
250
- // Don't count findings on lines outside the patched files — but our
251
- // fileContents map IS the patched files, so every finding is in-scope.
252
- return {
253
- ok: introducedHighOrAbove.length === 0,
254
- reason: introducedHighOrAbove.length === 0 ? 'verified' : 'introduced-new-findings',
255
- introduced: introducedHighOrAbove.map(f => ({
256
- vuln: f.vuln, file: f.file, line: f.line, severity: f.severity,
257
- stableId: f.stableId,
258
- })),
259
- };
260
- }
261
-
262
- // Detect which linter the project uses and run it on the patched files.
263
- // Returns { ok, runner, output } or { ok: true, runner: 'none' } when no
264
- // linter is configured (silent pass).
265
- function runProjectLinter(scanRoot, filePaths) {
266
- if (!scanRoot || !Array.isArray(filePaths) || filePaths.length === 0) {
267
- return { ok: true, runner: 'none' };
268
- }
269
- const has = (p) => { try { return external_node_fs_.existsSync(external_node_path_.join(scanRoot, p)); } catch { return false; } };
270
- // Pick the linter by config file present in the repo root.
271
- const jsFiles = filePaths.filter(f => /\.(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(f));
272
- const pyFiles = filePaths.filter(f => /\.py$/i.test(f));
273
- const goFiles = filePaths.filter(f => /\.go$/i.test(f));
274
- const javaFiles = filePaths.filter(f => /\.java$/i.test(f));
275
-
276
- if (jsFiles.length && (has('.eslintrc') || has('.eslintrc.json') || has('.eslintrc.js') || has('eslint.config.js') || has('eslint.config.mjs'))) {
277
- return runLinter(scanRoot, 'eslint', ['--no-error-on-unmatched-pattern', ...jsFiles]);
278
- }
279
- if (pyFiles.length && (has('pyproject.toml') || has('ruff.toml') || has('.ruff.toml'))) {
280
- return runLinter(scanRoot, 'ruff', ['check', ...pyFiles]);
281
- }
282
- if (pyFiles.length && has('.flake8')) {
283
- return runLinter(scanRoot, 'flake8', pyFiles);
284
- }
285
- if (goFiles.length && (has('.golangci.yml') || has('.golangci.yaml'))) {
286
- return runLinter(scanRoot, 'golangci-lint', ['run', ...goFiles]);
287
- }
288
- if (javaFiles.length && has('checkstyle.xml')) {
289
- return runLinter(scanRoot, 'checkstyle', ['-c', 'checkstyle.xml', ...javaFiles]);
290
- }
291
- return { ok: true, runner: 'none' };
292
- }
293
-
294
- function runLinter(cwd, cmd, args) {
295
- let r;
296
- try {
297
- r = (0,external_node_child_process_.spawnSync)(cmd, args, { cwd, encoding: 'utf8', timeout: 60_000 });
298
- } catch (e) {
299
- return { ok: true, runner: cmd, skipped: true, reason: 'binary-missing', error: e.message };
300
- }
301
- if (r.error && r.error.code === 'ENOENT') {
302
- return { ok: true, runner: cmd, skipped: true, reason: 'binary-missing' };
303
- }
304
- if (r.status === null) {
305
- return { ok: false, runner: cmd, reason: 'timed-out', output: (r.stderr || r.stdout || '').slice(-2000) };
306
- }
307
- return {
308
- ok: r.status === 0,
309
- runner: cmd,
310
- exitCode: r.status,
311
- output: ((r.stderr || '') + (r.stdout || '')).slice(-2000),
312
- };
313
- }
314
-
315
- // Top-level verify: re-scan + lint. Returns the combined verdict + a
316
- // human-readable summary string suitable for surfacing to the user.
317
- // Addition #7 — deterministic honesty gates on fix output. When the caller
318
- // supplies `fixMeta` ({ residual, verdict, evidence, signals }) — e.g. the
319
- // security-fixer agent's residual-risk text + completeness signals — the fix's
320
- // claims are checked mechanically (no hand-wave residual prose, a cited
321
- // file:line for any FP/safe verdict, and a FULL/MITIGATION/WORKAROUND tier). A
322
- // dishonest or over-claiming fix fails the gate. When `fixMeta` is absent
323
- // (the deterministic MCP write path, which has no claims to check) the honesty
324
- // gate is skipped and behavior is unchanged.
325
- async function verifyFix({
326
- scanRoot,
327
- originalFindingStableId,
328
- files,
329
- depFileContents,
330
- fixMeta,
331
- } = {}) {
332
- const rescan = await verifyPatch({ scanRoot, originalFindingStableId, files, depFileContents });
333
- const lint = runProjectLinter(scanRoot, Object.keys(files || {}));
334
- let honesty = null;
335
- if (fixMeta && typeof fixMeta === 'object') {
336
- try { honesty = gateFixOutput(fixMeta); } catch { honesty = null; }
337
- }
338
- const ok = rescan.ok && (lint.ok || lint.skipped) && (honesty ? honesty.ok : true);
339
- const summary = [
340
- `re-scan: ${rescan.ok ? 'PASS' : 'FAIL — ' + rescan.reason}`,
341
- `linter: ${lint.runner === 'none' ? 'skipped (no linter config)'
342
- : lint.skipped ? `${lint.runner} not installed`
343
- : lint.ok ? `${lint.runner} PASS`
344
- : `${lint.runner} FAIL (exit ${lint.exitCode})`}`,
345
- honesty ? `honesty: ${honesty.ok ? `PASS (${honesty.tier})` : 'FAIL — ' + honesty.violations.join('; ')}` : null,
346
- ].filter(Boolean).join('\n');
347
- return { ok, rescan, lint, honesty, summary };
348
- }
349
-
350
-
351
- /***/ })
352
-
353
- };