@clear-capabilities/agentic-security-scanner 0.195.22 → 0.195.24
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 +26 -0
- package/dist/3596.index.js +1 -1
- package/dist/550.index.js +9 -8
- package/dist/5620.index.js +59 -10
- package/dist/5937.index.js +88 -16
- package/dist/7838.index.js +32 -5
- package/dist/9441.index.js +50 -7
- package/dist/9824.index.js +19 -13
- package/dist/agentic-security.mjs +9 -9
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/{c1e282247b06d51c8ffb.json → daefda0819bb18309b75.json} +3 -3
- package/package.json +3 -3
- package/src/discovery/detector-spec-synthesis.js +3 -3
- package/src/discovery/llm-invoke.js +3 -3
- package/src/egress/transport.js +148 -0
- package/src/lineage/CLAUDE.md +12 -0
- package/src/lineage/DESIGN_GRAPH_BUILDER.md +23 -0
- package/src/lineage/driver.js +19 -2
- package/src/lineage/engine.js +24 -1
- package/src/lineage/graph-builder.js +3 -1
- package/src/lineage/lineage-source-catalog.js +143 -0
- package/src/lineage/source-seeding.js +142 -16
- package/src/lineage/summaries.js +21 -5
- package/src/llm-validator/index.js +4 -2
- package/src/llm-validator/local-endpoint.js +4 -1
- package/src/llm-validator/ollama-provider.js +8 -6
- package/src/posture/adversary-agent.js +27 -10
- package/src/posture/custom-rules.js +13 -1
- package/src/posture/fleet/cli.js +2 -4
- package/src/posture/fleet/fleet-manifest.js +4 -1
- package/src/posture/fleet/job-runner.js +28 -4
- package/src/posture/fleet/org-health.js +23 -0
- package/src/posture/fleet/scan-cache.js +1 -1
- package/src/posture/flow-narration.js +10 -8
- package/src/posture/llm-redteam.js +3 -1
- package/src/posture/provenance/coordinator.js +8 -0
- package/src/posture/provenance/origin-resolver.js +17 -4
- package/src/posture/rule-compiler/cli.js +56 -8
- package/src/posture/rule-compiler/corpus.js +10 -2
- package/src/posture/rule-compiler/ir.js +10 -1
- package/src/posture/rule-compiler/promotion.js +12 -2
- package/src/posture/rule-compiler/regression.js +1 -1
- package/src/posture/rule-compiler/store.js +27 -7
- package/src/posture/strategic-metrics.js +50 -7
- package/src/sca/llm-function-extract.js +17 -13
|
@@ -63,7 +63,7 @@ export function evidenceConversion(findings) {
|
|
|
63
63
|
const byState = Object.fromEntries(EVIDENCE_STATES.map((s) => [s, 0]));
|
|
64
64
|
for (const f of list) byState[evidenceStateOf(f)]++;
|
|
65
65
|
const converted = list.length - byState['single-signal'];
|
|
66
|
-
return { ...rate(converted, list.length), byState, basis: 'findings that reached corroborated or stronger, over all findings' };
|
|
66
|
+
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' };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const strongest = (a) => {
|
|
@@ -72,21 +72,52 @@ const strongest = (a) => {
|
|
|
72
72
|
return disappeared && a.testsRan === true && a.testsOk !== false && a.pocOk === true;
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
+
const isoOrNull = (v) => (typeof v === 'string' && !Number.isNaN(Date.parse(v)) ? v : null);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* One attempt per finding (GA-11): a fix tried five times is one remediation, not five, so the LATEST recorded attempt for each
|
|
79
|
+
* finding id is the one counted and the earlier ones are reported as `repeated`. An attempt with no finding id cannot be matched to
|
|
80
|
+
* another and counts on its own. Order is by `at` where every record has one, else by position in the log.
|
|
81
|
+
*/
|
|
82
|
+
function dedupeAttempts(attempts) {
|
|
83
|
+
const list = Array.isArray(attempts) ? attempts.filter((a) => a && typeof a === 'object') : [];
|
|
84
|
+
const latest = new Map();
|
|
85
|
+
const loose = [];
|
|
86
|
+
list.forEach((a, i) => {
|
|
87
|
+
const key = typeof a.stableId === 'string' && a.stableId ? a.stableId : (typeof a.findingId === 'string' && a.findingId ? a.findingId : null);
|
|
88
|
+
if (!key) { loose.push(a); return; }
|
|
89
|
+
const prev = latest.get(key);
|
|
90
|
+
const newer = !prev || (isoOrNull(a.at) && isoOrNull(prev.at) ? Date.parse(a.at) >= Date.parse(prev.at) : i > prev._i);
|
|
91
|
+
if (newer) latest.set(key, Object.assign(Object.create(null), a, { _i: i }));
|
|
92
|
+
});
|
|
93
|
+
const kept = [...latest.values()].map((a) => { const { _i, ...rest } = a; return rest; }).concat(loose);
|
|
94
|
+
return { attempts: kept, recorded: list.length, repeated: list.length - kept.length };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The time span the recorded attempts cover: `from` and `to` are the earliest and latest `at`, or `null` when none carries one. */
|
|
98
|
+
function attemptsWindow(attempts) {
|
|
99
|
+
const times = (Array.isArray(attempts) ? attempts : []).map((a) => a && isoOrNull(a.at)).filter(Boolean).sort();
|
|
100
|
+
return { from: times[0] ?? null, to: times[times.length - 1] ?? null };
|
|
101
|
+
}
|
|
102
|
+
|
|
75
103
|
/** Of the fix attempts, how many reached the strongest verification tier. A lower bound: some classes have no independent proof to run. */
|
|
76
104
|
export function verifiedRemediationRate(attempts) {
|
|
77
|
-
const
|
|
105
|
+
const { attempts: list, recorded, repeated } = dedupeAttempts(attempts);
|
|
78
106
|
const full = list.filter(strongest).length;
|
|
79
107
|
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;
|
|
108
|
+
const failed = list.filter((a) => a.ok === false || a.verifiedFull === false).length;
|
|
80
109
|
return {
|
|
110
|
+
status: list.length ? 'measured' : 'unmeasured',
|
|
81
111
|
...rate(full, list.length),
|
|
82
|
-
disappearedOnly,
|
|
83
|
-
|
|
112
|
+
recorded, repeated, failed, disappearedOnly,
|
|
113
|
+
window: attemptsWindow(attempts),
|
|
114
|
+
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',
|
|
84
115
|
};
|
|
85
116
|
}
|
|
86
117
|
|
|
87
|
-
/** Nothing measures it independently of the rule compiler's own corpus, so it is reported as
|
|
118
|
+
/** Nothing measures it independently of the rule compiler's own corpus, so it is reported as unmeasured, with the reason. */
|
|
88
119
|
export function securityLearningRate() {
|
|
89
|
-
return { status: '
|
|
120
|
+
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' };
|
|
90
121
|
}
|
|
91
122
|
|
|
92
123
|
/**
|
|
@@ -95,11 +126,21 @@ export function securityLearningRate() {
|
|
|
95
126
|
* @param {object[]} [o.fixAttempts] fix verification attempts
|
|
96
127
|
*/
|
|
97
128
|
export function computeStrategicMetrics({ scan = null, fixAttempts = [] } = {}) {
|
|
129
|
+
const attempts = Array.isArray(fixAttempts) ? fixAttempts : [];
|
|
98
130
|
return {
|
|
99
131
|
schema: STRATEGIC_METRICS_SCHEMA, version: STRATEGIC_METRICS_VERSION,
|
|
132
|
+
// The scope and time basis (GA-11): these are per-scan figures for ONE scan plus the attempts recorded on this machine, never an
|
|
133
|
+
// organisation trend. The scan is named by its id and start time; the attempts by the span their timestamps cover.
|
|
134
|
+
scope: {
|
|
135
|
+
kind: 'per-scan',
|
|
136
|
+
scanId: scan && typeof scan.scanId === 'string' ? scan.scanId : null,
|
|
137
|
+
scanStartedAt: scan ? isoOrNull(scan.startedAt) : null,
|
|
138
|
+
attempts: { ...attemptsWindow(attempts), recorded: attempts.filter((a) => a && typeof a === 'object').length },
|
|
139
|
+
trend: 'not a trend: one scan and this machine\'s recorded attempts; compare scans by scanId to see movement',
|
|
140
|
+
},
|
|
100
141
|
assuranceCoverage: assuranceCoverage(scan && scan.scanHealth),
|
|
101
142
|
evidenceConversion: evidenceConversion(scan && scan.findings),
|
|
102
|
-
verifiedRemediationRate: verifiedRemediationRate(
|
|
143
|
+
verifiedRemediationRate: verifiedRemediationRate(attempts),
|
|
103
144
|
securityLearningRate: securityLearningRate(),
|
|
104
145
|
};
|
|
105
146
|
}
|
|
@@ -109,6 +150,7 @@ const pc = (r) => (r && r.percent !== null && r.percent !== undefined ? `${r.per
|
|
|
109
150
|
export function renderStrategicMetrics(m) {
|
|
110
151
|
const ac = m.assuranceCoverage; const ec = m.evidenceConversion; const vr = m.verifiedRemediationRate;
|
|
111
152
|
const L = [];
|
|
153
|
+
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`);
|
|
112
154
|
L.push(`Assurance Coverage: ${ac.percent === null ? 'not measurable' : `${ac.percent}% (the lower of the two components)`}${ac.scanStatus ? `; scan ${ac.scanStatus}` : ''}`);
|
|
113
155
|
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`);
|
|
114
156
|
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`);
|
|
@@ -117,6 +159,7 @@ export function renderStrategicMetrics(m) {
|
|
|
117
159
|
L.push(` ${EVIDENCE_STATES.map((s) => `${s} ${ec.byState[s]}`).join(', ')}`);
|
|
118
160
|
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' : ''}`);
|
|
119
161
|
if (vr.disappearedOnly) L.push(` ${vr.disappearedOnly} satisfied only the re-scan, which is also what deleting the code produces`);
|
|
162
|
+
if (vr.repeated) L.push(` ${vr.recorded} attempt(s) recorded; ${vr.repeated} earlier attempt(s) on the same finding counted once, by the latest`);
|
|
120
163
|
L.push(`Security Learning Rate: not measured. ${m.securityLearningRate.reason}`);
|
|
121
164
|
return L.join('\n');
|
|
122
165
|
}
|
|
@@ -10,6 +10,8 @@ import * as crypto from 'node:crypto';
|
|
|
10
10
|
import * as fs from 'node:fs';
|
|
11
11
|
import * as path from 'node:path';
|
|
12
12
|
import { evaluateEgress } from '../egress/policy.js';
|
|
13
|
+
import { modelFetch } from '../egress/transport.js';
|
|
14
|
+
import { resolveProvider, buildProviderRequest } from '../llm-validator/providers.js';
|
|
13
15
|
|
|
14
16
|
const CACHE_DIR = process.env.XDG_CONFIG_HOME
|
|
15
17
|
? path.join(process.env.XDG_CONFIG_HOME, 'agentic-security', 'llm-sca-cache')
|
|
@@ -37,24 +39,26 @@ export function isLlmScaEnabled() {
|
|
|
37
39
|
return process.env.AGENTIC_SECURITY_LLM_SCA === '1';
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
// The provider is resolved the same way the validator resolves it (GA-06.3): the `local` preset refuses a non-loopback endpoint here
|
|
43
|
+
// too, instead of this module reading the raw endpoint variable and calling it regardless. The Ollama provider speaks a different
|
|
44
|
+
// protocol and this extractor does not implement it, so it is treated as not configured.
|
|
45
|
+
function _endpointConfig(env = process.env) {
|
|
46
|
+
const r = resolveProvider({ role: 'validate', env });
|
|
47
|
+
if (!r.ok || r.config.provider === 'ollama') return null;
|
|
48
|
+
return { endpoint: r.config.endpoint, apiKey: r.config.apiKey, model: r.config.model || 'unknown', shape: r.config.shape };
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
async function _askLlm(prompt, config) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
const resp = await fetch(config.endpoint, {
|
|
52
|
+
const req = buildProviderRequest(config, prompt, 256);
|
|
53
|
+
const resp = await modelFetch(config.endpoint, {
|
|
51
54
|
method: 'POST',
|
|
52
|
-
headers,
|
|
53
|
-
body: JSON.stringify(
|
|
54
|
-
|
|
55
|
-
});
|
|
55
|
+
headers: req.headers,
|
|
56
|
+
body: JSON.stringify(req.body),
|
|
57
|
+
}, { timeoutMs: 15000 });
|
|
56
58
|
if (!resp.ok) return null;
|
|
57
|
-
|
|
59
|
+
let text = await resp.text();
|
|
60
|
+
// a vendor shape wraps the model's text in its own envelope; the generic shape's reply is searched as it came
|
|
61
|
+
try { const t = req.extractText(JSON.parse(text)); if (typeof t === 'string' && t) text = t; } catch { /* not an envelope */ }
|
|
58
62
|
const jsonMatch = text.match(/\{[^{}]*"functions"\s*:\s*\[[^\]]*\][^{}]*\}/);
|
|
59
63
|
if (!jsonMatch) return null;
|
|
60
64
|
try { return JSON.parse(jsonMatch[0]); } catch { return null; }
|