@clear-capabilities/agentic-security-scanner 0.195.22 → 0.195.23

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 (36) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/{c1e282247b06d51c8ffb.json → 01087ded263953062654.json} +2 -2
  3. package/dist/550.index.js +9 -8
  4. package/dist/5620.index.js +59 -10
  5. package/dist/5937.index.js +88 -16
  6. package/dist/7838.index.js +32 -5
  7. package/dist/9441.index.js +50 -7
  8. package/dist/9824.index.js +19 -13
  9. package/dist/agentic-security.mjs +2 -2
  10. package/dist/agentic-security.mjs.sha256 +1 -1
  11. package/package.json +2 -2
  12. package/src/discovery/detector-spec-synthesis.js +3 -3
  13. package/src/discovery/llm-invoke.js +3 -3
  14. package/src/egress/transport.js +148 -0
  15. package/src/llm-validator/index.js +4 -2
  16. package/src/llm-validator/local-endpoint.js +4 -1
  17. package/src/llm-validator/ollama-provider.js +8 -6
  18. package/src/posture/adversary-agent.js +27 -10
  19. package/src/posture/custom-rules.js +13 -1
  20. package/src/posture/fleet/cli.js +2 -4
  21. package/src/posture/fleet/fleet-manifest.js +4 -1
  22. package/src/posture/fleet/job-runner.js +28 -4
  23. package/src/posture/fleet/org-health.js +23 -0
  24. package/src/posture/fleet/scan-cache.js +1 -1
  25. package/src/posture/flow-narration.js +10 -8
  26. package/src/posture/llm-redteam.js +3 -1
  27. package/src/posture/provenance/coordinator.js +8 -0
  28. package/src/posture/provenance/origin-resolver.js +17 -4
  29. package/src/posture/rule-compiler/cli.js +56 -8
  30. package/src/posture/rule-compiler/corpus.js +10 -2
  31. package/src/posture/rule-compiler/ir.js +10 -1
  32. package/src/posture/rule-compiler/promotion.js +12 -2
  33. package/src/posture/rule-compiler/regression.js +1 -1
  34. package/src/posture/rule-compiler/store.js +27 -7
  35. package/src/posture/strategic-metrics.js +50 -7
  36. package/src/sca/llm-function-extract.js +17 -13
@@ -89,7 +89,7 @@ function evidenceConversion(findings) {
89
89
  const byState = Object.fromEntries(EVIDENCE_STATES.map((s) => [s, 0]));
90
90
  for (const f of list) byState[evidenceStateOf(f)]++;
91
91
  const converted = list.length - byState['single-signal'];
92
- return { ...rate(converted, list.length), byState, basis: 'findings that reached corroborated or stronger, over all findings' };
92
+ return { status: list.length ? 'measured' : 'unmeasured', ...rate(converted, list.length), byState, basis: 'findings that reached corroborated or stronger, over all findings of the last scan' };
93
93
  }
94
94
 
95
95
  const strongest = (a) => {
@@ -98,21 +98,52 @@ const strongest = (a) => {
98
98
  return disappeared && a.testsRan === true && a.testsOk !== false && a.pocOk === true;
99
99
  };
100
100
 
101
+ const isoOrNull = (v) => (typeof v === 'string' && !Number.isNaN(Date.parse(v)) ? v : null);
102
+
103
+ /**
104
+ * One attempt per finding (GA-11): a fix tried five times is one remediation, not five, so the LATEST recorded attempt for each
105
+ * finding id is the one counted and the earlier ones are reported as `repeated`. An attempt with no finding id cannot be matched to
106
+ * another and counts on its own. Order is by `at` where every record has one, else by position in the log.
107
+ */
108
+ function dedupeAttempts(attempts) {
109
+ const list = Array.isArray(attempts) ? attempts.filter((a) => a && typeof a === 'object') : [];
110
+ const latest = new Map();
111
+ const loose = [];
112
+ list.forEach((a, i) => {
113
+ const key = typeof a.stableId === 'string' && a.stableId ? a.stableId : (typeof a.findingId === 'string' && a.findingId ? a.findingId : null);
114
+ if (!key) { loose.push(a); return; }
115
+ const prev = latest.get(key);
116
+ const newer = !prev || (isoOrNull(a.at) && isoOrNull(prev.at) ? Date.parse(a.at) >= Date.parse(prev.at) : i > prev._i);
117
+ if (newer) latest.set(key, Object.assign(Object.create(null), a, { _i: i }));
118
+ });
119
+ const kept = [...latest.values()].map((a) => { const { _i, ...rest } = a; return rest; }).concat(loose);
120
+ return { attempts: kept, recorded: list.length, repeated: list.length - kept.length };
121
+ }
122
+
123
+ /** The time span the recorded attempts cover: `from` and `to` are the earliest and latest `at`, or `null` when none carries one. */
124
+ function attemptsWindow(attempts) {
125
+ const times = (Array.isArray(attempts) ? attempts : []).map((a) => a && isoOrNull(a.at)).filter(Boolean).sort();
126
+ return { from: times[0] ?? null, to: times[times.length - 1] ?? null };
127
+ }
128
+
101
129
  /** Of the fix attempts, how many reached the strongest verification tier. A lower bound: some classes have no independent proof to run. */
102
130
  function verifiedRemediationRate(attempts) {
103
- const list = Array.isArray(attempts) ? attempts.filter((a) => a && typeof a === 'object') : [];
131
+ const { attempts: list, recorded, repeated } = dedupeAttempts(attempts);
104
132
  const full = list.filter(strongest).length;
105
133
  const disappearedOnly = list.filter((a) => typeof a.verifiedFull !== 'boolean' && (a.rescanOk === true || (a.ok === true && a.rescanOk !== false)) && !(a.testsRan === true && a.testsOk !== false) && a.pocOk !== true).length;
134
+ const failed = list.filter((a) => a.ok === false || a.verifiedFull === false).length;
106
135
  return {
136
+ status: list.length ? 'measured' : 'unmeasured',
107
137
  ...rate(full, list.length),
108
- disappearedOnly,
109
- basis: 'attempts that passed the re-scan, the project tests and the independent attack replay, over all attempts. A lower bound where a class has no replay',
138
+ recorded, repeated, failed, disappearedOnly,
139
+ window: attemptsWindow(attempts),
140
+ basis: 'the latest attempt per finding that passed the re-scan, the project tests and the independent attack replay, over the latest attempt per finding. A lower bound where a class has no replay',
110
141
  };
111
142
  }
112
143
 
113
- /** Nothing measures it independently of the rule compiler's own corpus, so it is reported as not measured. */
144
+ /** Nothing measures it independently of the rule compiler's own corpus, so it is reported as unmeasured, with the reason. */
114
145
  function securityLearningRate() {
115
- return { status: 'not-measured', reason: 'the rule compiler grades its rules against a corpus built from the same finding; no independent measurement of novel weaknesses converted into reusable knowledge exists' };
146
+ return { status: 'unmeasured', reason: 'the rule compiler grades its rules against a corpus built from the same finding and the reviewer-supplied independent examples; no independent longitudinal measurement of novel weaknesses converted into reusable knowledge exists' };
116
147
  }
117
148
 
118
149
  /**
@@ -121,11 +152,21 @@ function securityLearningRate() {
121
152
  * @param {object[]} [o.fixAttempts] fix verification attempts
122
153
  */
123
154
  function computeStrategicMetrics({ scan = null, fixAttempts = [] } = {}) {
155
+ const attempts = Array.isArray(fixAttempts) ? fixAttempts : [];
124
156
  return {
125
157
  schema: STRATEGIC_METRICS_SCHEMA, version: STRATEGIC_METRICS_VERSION,
158
+ // The scope and time basis (GA-11): these are per-scan figures for ONE scan plus the attempts recorded on this machine, never an
159
+ // organisation trend. The scan is named by its id and start time; the attempts by the span their timestamps cover.
160
+ scope: {
161
+ kind: 'per-scan',
162
+ scanId: scan && typeof scan.scanId === 'string' ? scan.scanId : null,
163
+ scanStartedAt: scan ? isoOrNull(scan.startedAt) : null,
164
+ attempts: { ...attemptsWindow(attempts), recorded: attempts.filter((a) => a && typeof a === 'object').length },
165
+ trend: 'not a trend: one scan and this machine\'s recorded attempts; compare scans by scanId to see movement',
166
+ },
126
167
  assuranceCoverage: assuranceCoverage(scan && scan.scanHealth),
127
168
  evidenceConversion: evidenceConversion(scan && scan.findings),
128
- verifiedRemediationRate: verifiedRemediationRate(fixAttempts),
169
+ verifiedRemediationRate: verifiedRemediationRate(attempts),
129
170
  securityLearningRate: securityLearningRate(),
130
171
  };
131
172
  }
@@ -135,6 +176,7 @@ const pc = (r) => (r && r.percent !== null && r.percent !== undefined ? `${r.per
135
176
  function renderStrategicMetrics(m) {
136
177
  const ac = m.assuranceCoverage; const ec = m.evidenceConversion; const vr = m.verifiedRemediationRate;
137
178
  const L = [];
179
+ if (m.scope) L.push(`Scope: one scan${m.scope.scanId ? ` (${m.scope.scanId}${m.scope.scanStartedAt ? `, started ${m.scope.scanStartedAt}` : ''})` : ''} and ${m.scope.attempts.recorded} fix attempt(s) recorded on this machine${m.scope.attempts.from ? ` between ${m.scope.attempts.from} and ${m.scope.attempts.to}` : ''}; not a trend`);
138
180
  L.push(`Assurance Coverage: ${ac.percent === null ? 'not measurable' : `${ac.percent}% (the lower of the two components)`}${ac.scanStatus ? `; scan ${ac.scanStatus}` : ''}`);
139
181
  if (ac.files) L.push(` files: ${ac.files.numerator} of ${ac.files.denominator} completed (${pc(ac.files)}); ${ac.files.skipped} skipped, ${ac.files.unanalysed} not analysed, ${ac.files.timedOut} timed out`);
140
182
  if (ac.analyzers) L.push(` analyzers: ${ac.analyzers.numerator} of ${ac.analyzers.denominator} completed (${pc(ac.analyzers)}); ${ac.analyzers.failed} failed, ${ac.analyzers.timedOut} timed out`);
@@ -143,6 +185,7 @@ function renderStrategicMetrics(m) {
143
185
  L.push(` ${EVIDENCE_STATES.map((s) => `${s} ${ec.byState[s]}`).join(', ')}`);
144
186
  L.push(`Verified Remediation Rate: ${vr.numerator} of ${vr.denominator} fix attempts reached the strongest verification tier (${pc(vr)})${vr.denominator ? '; a lower bound where a class has no attack replay' : ''}`);
145
187
  if (vr.disappearedOnly) L.push(` ${vr.disappearedOnly} satisfied only the re-scan, which is also what deleting the code produces`);
188
+ if (vr.repeated) L.push(` ${vr.recorded} attempt(s) recorded; ${vr.repeated} earlier attempt(s) on the same finding counted once, by the latest`);
146
189
  L.push(`Security Learning Rate: not measured. ${m.securityLearningRate.reason}`);
147
190
  return L.join('\n');
148
191
  }
@@ -13,6 +13,8 @@ export const modules = {
13
13
  /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(73024);
14
14
  /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);
15
15
  /* harmony import */ var _egress_policy_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(45712);
16
+ /* harmony import */ var _egress_transport_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(73785);
17
+ /* harmony import */ var _llm_validator_providers_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(38947);
16
18
  // LLM-assisted vulnerable function extraction for SCA findings.
17
19
  //
18
20
  // For CVEs without OSV ecosystem_specific data or GHSA fix commits,
@@ -26,6 +28,8 @@ export const modules = {
26
28
 
27
29
 
28
30
 
31
+
32
+
29
33
  const CACHE_DIR = process.env.XDG_CONFIG_HOME
30
34
  ? node_path__WEBPACK_IMPORTED_MODULE_2__.join(process.env.XDG_CONFIG_HOME, 'agentic-security', 'llm-sca-cache')
31
35
  : node_path__WEBPACK_IMPORTED_MODULE_2__.join(process.env.HOME || '/tmp', '.config', 'agentic-security', 'llm-sca-cache');
@@ -52,24 +56,26 @@ function isLlmScaEnabled() {
52
56
  return process.env.AGENTIC_SECURITY_LLM_SCA === '1';
53
57
  }
54
58
 
55
- function _endpointConfig() {
56
- const endpoint = process.env.AGENTIC_SECURITY_LLM_ENDPOINT;
57
- const apiKey = process.env.AGENTIC_SECURITY_LLM_API_KEY;
58
- const model = process.env.AGENTIC_SECURITY_LLM_MODEL || 'unknown';
59
- return endpoint ? { endpoint, apiKey, model } : null;
59
+ // The provider is resolved the same way the validator resolves it (GA-06.3): the `local` preset refuses a non-loopback endpoint here
60
+ // too, instead of this module reading the raw endpoint variable and calling it regardless. The Ollama provider speaks a different
61
+ // protocol and this extractor does not implement it, so it is treated as not configured.
62
+ function _endpointConfig(env = process.env) {
63
+ const r = (0,_llm_validator_providers_js__WEBPACK_IMPORTED_MODULE_5__.resolveProvider)({ role: 'validate', env });
64
+ if (!r.ok || r.config.provider === 'ollama') return null;
65
+ return { endpoint: r.config.endpoint, apiKey: r.config.apiKey, model: r.config.model || 'unknown', shape: r.config.shape };
60
66
  }
61
67
 
62
68
  async function _askLlm(prompt, config) {
63
- const headers = { 'Content-Type': 'application/json' };
64
- if (config.apiKey) headers['Authorization'] = `Bearer ${config.apiKey}`;
65
- const resp = await fetch(config.endpoint, {
69
+ const req = (0,_llm_validator_providers_js__WEBPACK_IMPORTED_MODULE_5__/* .buildProviderRequest */ .Z$)(config, prompt, 256);
70
+ const resp = await (0,_egress_transport_js__WEBPACK_IMPORTED_MODULE_4__/* .modelFetch */ .Ye)(config.endpoint, {
66
71
  method: 'POST',
67
- headers,
68
- body: JSON.stringify({ prompt, model: config.model }),
69
- signal: AbortSignal.timeout(15000),
70
- });
72
+ headers: req.headers,
73
+ body: JSON.stringify(req.body),
74
+ }, { timeoutMs: 15000 });
71
75
  if (!resp.ok) return null;
72
- const text = await resp.text();
76
+ let text = await resp.text();
77
+ // a vendor shape wraps the model's text in its own envelope; the generic shape's reply is searched as it came
78
+ try { const t = req.extractText(JSON.parse(text)); if (typeof t === 'string' && t) text = t; } catch { /* not an envelope */ }
73
79
  const jsonMatch = text.match(/\{[^{}]*"functions"\s*:\s*\[[^\]]*\][^{}]*\}/);
74
80
  if (!jsonMatch) return null;
75
81
  try { return JSON.parse(jsonMatch[0]); } catch { return null; }