@clear-capabilities/agentic-security-scanner 0.79.0 → 0.80.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 (91) hide show
  1. package/dist/178.index.js +1 -1
  2. package/dist/333.index.js +283 -0
  3. package/dist/384.index.js +1 -1
  4. package/dist/637.index.js +1 -1
  5. package/dist/838.index.js +1 -1
  6. package/dist/985.index.js +90 -1
  7. package/dist/agentic-security.mjs +83 -83
  8. package/dist/agentic-security.mjs.sha256 +1 -1
  9. package/package.json +6 -4
  10. package/src/.agentic-security/findings.json +104638 -0
  11. package/src/.agentic-security/last-scan.json +104638 -0
  12. package/src/.agentic-security/last-scan.json.sig +1 -0
  13. package/src/.agentic-security/scan-history.json +12562 -0
  14. package/src/.agentic-security/streak.json +21 -0
  15. package/src/dataflow/.agentic-security/findings.json +6086 -0
  16. package/src/dataflow/.agentic-security/last-scan.json +6086 -0
  17. package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
  18. package/src/dataflow/.agentic-security/scan-history.json +250 -0
  19. package/src/dataflow/.agentic-security/streak.json +21 -0
  20. package/src/dataflow/cross-service-taint.js +201 -0
  21. package/src/dataflow/formal-verify.js +204 -0
  22. package/src/dataflow/ifds-precise.js +222 -0
  23. package/src/dataflow/k2-summary-cache.js +153 -0
  24. package/src/dataflow/lib-taint-summaries.js +198 -0
  25. package/src/dataflow/privacy-taint.js +205 -0
  26. package/src/dataflow/smt-feasibility.js +189 -0
  27. package/src/engine.js +784 -127
  28. package/src/ir/.agentic-security/findings.json +4011 -0
  29. package/src/ir/.agentic-security/last-scan.json +4011 -0
  30. package/src/ir/.agentic-security/last-scan.json.sig +1 -0
  31. package/src/ir/.agentic-security/scan-history.json +193 -0
  32. package/src/ir/.agentic-security/streak.json +20 -0
  33. package/src/ir/cpp-preprocessor.js +142 -0
  34. package/src/ir/csharp-ir.js +604 -0
  35. package/src/ir/universal-ir.js +403 -0
  36. package/src/mcp/.agentic-security/findings.json +8632 -0
  37. package/src/mcp/.agentic-security/last-scan.json +8632 -0
  38. package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
  39. package/src/mcp/.agentic-security/scan-history.json +143 -0
  40. package/src/mcp/.agentic-security/streak.json +20 -0
  41. package/src/mcp/tools.js +90 -1
  42. package/src/posture/.agentic-security/findings.json +64004 -0
  43. package/src/posture/.agentic-security/last-scan.json +64004 -0
  44. package/src/posture/.agentic-security/last-scan.json.sig +1 -0
  45. package/src/posture/.agentic-security/scan-history.json +7162 -0
  46. package/src/posture/.agentic-security/streak.json +21 -0
  47. package/src/posture/api-contract.js +193 -0
  48. package/src/posture/attack-taxonomy.js +227 -0
  49. package/src/posture/compliance-policy.js +218 -0
  50. package/src/posture/composite-risk.js +122 -0
  51. package/src/posture/csharp-analysis.js +330 -0
  52. package/src/posture/exploit-bundle.js +210 -0
  53. package/src/posture/federated-learning.js +172 -0
  54. package/src/posture/license-attributions.js +94 -0
  55. package/src/posture/license-graph.js +238 -0
  56. package/src/posture/pqc-migration-plan.js +158 -0
  57. package/src/posture/reachability-filter.js +33 -2
  58. package/src/posture/realtime-cve-monitor.js +214 -0
  59. package/src/posture/runtime-correlation.js +174 -0
  60. package/src/posture/sbom-diff.js +171 -0
  61. package/src/posture/sca-policy.js +235 -0
  62. package/src/posture/sca-upgrade.js +259 -0
  63. package/src/posture/threat-model-auto.js +268 -0
  64. package/src/posture/triage-learning.js +170 -0
  65. package/src/posture/triage.js +26 -1
  66. package/src/sast/.agentic-security/findings.json +6154 -0
  67. package/src/sast/.agentic-security/last-scan.json +6154 -0
  68. package/src/sast/.agentic-security/last-scan.json.sig +1 -0
  69. package/src/sast/.agentic-security/scan-history.json +941 -0
  70. package/src/sast/.agentic-security/streak.json +22 -0
  71. package/src/sast/_secret-entropy.js +145 -0
  72. package/src/sast/cloud-iam.js +312 -0
  73. package/src/sast/cpp.js +138 -4
  74. package/src/sast/crypto-protocol.js +388 -0
  75. package/src/sast/csharp-tokenizer.js +392 -0
  76. package/src/sast/csharp.js +924 -138
  77. package/src/sast/dapp-frontend.js +200 -0
  78. package/src/sast/k8s-admission.js +271 -0
  79. package/src/sast/llm-app.js +272 -0
  80. package/src/sast/ml-supply-chain.js +259 -0
  81. package/src/sast/mobile.js +224 -0
  82. package/src/sast/post-quantum-crypto.js +348 -0
  83. package/src/sast/web3-advanced.js +375 -0
  84. package/src/sca/.agentic-security/findings.json +7460 -0
  85. package/src/sca/.agentic-security/last-scan.json +7460 -0
  86. package/src/sca/.agentic-security/last-scan.json.sig +1 -0
  87. package/src/sca/.agentic-security/scan-history.json +113 -0
  88. package/src/sca/.agentic-security/streak.json +21 -0
  89. package/src/sca/CLAUDE.md +161 -0
  90. package/src/sca/binary-metadata.js +37 -15
  91. package/src/sca/sigstore-verify.js +215 -0
@@ -0,0 +1,210 @@
1
+ // AI-native exploit PoC + regression test + remediation diff bundle —
2
+ // Recommendation #6 of the world-class roadmap.
3
+ //
4
+ // For each high-confidence finding, emit a 4-piece bundle:
5
+ // 1. PoC script — a working curl / Python / Node command that
6
+ // triggers the finding against the live app
7
+ // 2. Regression test — a framework-idiomatic test (Jest / pytest /
8
+ // JUnit) that exercises the vulnerable path
9
+ // and asserts on the broken behavior
10
+ // 3. Remediation — the patched source code
11
+ // 4. Diff — git-applyable patch of #3 against the working tree
12
+ //
13
+ // All four artifacts ship together so the developer's path from
14
+ // "scanner shows me a finding" to "PR landed with fix + test" is single-click.
15
+ //
16
+ // The four pieces are *deterministic* per family — we DON'T rely on
17
+ // the LLM to generate them at scan time. Instead we use a per-family
18
+ // template catalog that produces concrete artifacts from the finding's
19
+ // IR context (file, line, source variable, sink expression, etc.).
20
+ //
21
+ // LLM validation (already wired in scanner/src/llm-validator/) can run
22
+ // AFTER bundle emission to confirm the PoC is plausible — but bundle
23
+ // generation itself is deterministic so --deterministic SARIF stays
24
+ // reproducible run-over-run.
25
+
26
+ const TEMPLATES = {
27
+ 'sql-injection': {
28
+ pocCurl: (f) => `# PoC: SQL Injection at ${f.file}:${f.line}
29
+ # The vulnerable code concatenates user input into a SQL query.
30
+ # Sending an injected payload extracts the database.users table:
31
+ curl -X GET '${f._inRoute?.path || '/UPDATE_ME'}?${(f._sourceParam || 'id')}=1%27%20UNION%20SELECT%20username,password%20FROM%20users--' \\
32
+ -H 'Accept: application/json'
33
+ # Expected vulnerable behavior: response includes usernames + password hashes
34
+ # Expected fixed behavior: response is a 4xx error or returns no rows`,
35
+ regressionTestJest: (f) => `// Regression test for ${f.cwe} at ${f.file}:${f.line}
36
+ import request from 'supertest';
37
+ import app from '../app';
38
+ describe('${f.vuln}', () => {
39
+ it('rejects SQL injection payloads', async () => {
40
+ const payload = "1' UNION SELECT username,password FROM users--";
41
+ const res = await request(app).get('${f._inRoute?.path || '/UPDATE_ME'}').query({ id: payload });
42
+ expect(res.status).toBeGreaterThanOrEqual(400);
43
+ expect(JSON.stringify(res.body)).not.toMatch(/password|username/i);
44
+ });
45
+ });`,
46
+ regressionTestPytest: (f) => `# Regression test for ${f.cwe} at ${f.file}:${f.line}
47
+ import pytest
48
+ from app import app as flask_app
49
+
50
+ @pytest.fixture
51
+ def client():
52
+ return flask_app.test_client()
53
+
54
+ def test_sql_injection_rejected(client):
55
+ payload = "1' UNION SELECT username,password FROM users--"
56
+ resp = client.get("${f._inRoute?.path || '/UPDATE_ME'}", query_string={'id': payload})
57
+ assert resp.status_code >= 400, "Vulnerable endpoint returned 2xx for SQL injection payload"
58
+ body = resp.get_data(as_text=True)
59
+ assert 'password' not in body.lower(), "Response leaked password column"`,
60
+ remediationSummary: () =>
61
+ 'Parameterize the SQL query: use ? placeholders (or named parameters) + a bind-value call instead of concatenating user input. The driver escapes parameter values; concatenation does not.',
62
+ },
63
+
64
+ 'xss': {
65
+ pocCurl: (f) => `# PoC: Reflected XSS at ${f.file}:${f.line}
66
+ curl -X GET '${f._inRoute?.path || '/UPDATE_ME'}?${(f._sourceParam || 'name')}=%3Cscript%3Ealert(1)%3C%2Fscript%3E'
67
+ # Expected vulnerable behavior: response body contains the literal <script>alert(1)</script>
68
+ # Expected fixed behavior: response body contains the HTML-encoded form &lt;script&gt;…`,
69
+ regressionTestJest: (f) => `import request from 'supertest';
70
+ import app from '../app';
71
+ describe('${f.vuln}', () => {
72
+ it('HTML-encodes user input', async () => {
73
+ const res = await request(app).get('${f._inRoute?.path || '/UPDATE_ME'}').query({ name: '<script>alert(1)</script>' });
74
+ expect(res.text).not.toContain('<script>alert(1)</script>');
75
+ expect(res.text).toMatch(/&lt;script&gt;|&amp;lt;|encoded/i);
76
+ });
77
+ });`,
78
+ regressionTestPytest: (f) => `import pytest
79
+ from app import app as flask_app
80
+
81
+ @pytest.fixture
82
+ def client():
83
+ return flask_app.test_client()
84
+
85
+ def test_xss_encoded(client):
86
+ resp = client.get("${f._inRoute?.path || '/UPDATE_ME'}", query_string={'name': '<script>alert(1)</script>'})
87
+ body = resp.get_data(as_text=True)
88
+ assert '<script>alert(1)</script>' not in body, "XSS payload reflected unencoded"`,
89
+ remediationSummary: () =>
90
+ `Encode user input via the framework's built-in escaper (Razor @x, Django escape, Jinja autoescape, React JSX). Replace any explicit "raw"/"unsafe" output methods with their encoded equivalent.`,
91
+ },
92
+
93
+ 'command-injection': {
94
+ pocCurl: (f) => `# PoC: Command Injection at ${f.file}:${f.line}
95
+ curl -X POST '${f._inRoute?.path || '/UPDATE_ME'}' \\
96
+ -H 'Content-Type: application/json' \\
97
+ -d '{"${f._sourceParam || 'name'}": "; sleep 5; #"}'
98
+ # Expected vulnerable behavior: response time > 5 seconds (sleep ran via shell)
99
+ # Expected fixed behavior: response time normal, "sleep" appears as literal arg`,
100
+ regressionTestJest: (f) => `import request from 'supertest';
101
+ import app from '../app';
102
+ describe('${f.vuln}', () => {
103
+ it('does not invoke shell metacharacters', async () => {
104
+ const t0 = Date.now();
105
+ await request(app).post('${f._inRoute?.path || '/UPDATE_ME'}').send({ name: '; sleep 5; #' });
106
+ const elapsed = Date.now() - t0;
107
+ expect(elapsed).toBeLessThan(2000);
108
+ });
109
+ });`,
110
+ remediationSummary: () =>
111
+ `Replace the system()/exec()/shell-out call with the argv form: pass the program path as a constant and arguments as a separate list/array. The shell parser is bypassed, and metacharacters are treated as literal args.`,
112
+ },
113
+
114
+ 'path-traversal': {
115
+ pocCurl: (f) => `# PoC: Path Traversal at ${f.file}:${f.line}
116
+ curl -X GET '${f._inRoute?.path || '/UPDATE_ME'}?${(f._sourceParam || 'file')}=../../../etc/passwd'
117
+ # Expected vulnerable behavior: response contains "root:" / passwd file format
118
+ # Expected fixed behavior: response is a 4xx or "file not found"`,
119
+ regressionTestJest: (f) => `import request from 'supertest';
120
+ import app from '../app';
121
+ describe('${f.vuln}', () => {
122
+ it('rejects path traversal sequences', async () => {
123
+ const res = await request(app).get('${f._inRoute?.path || '/UPDATE_ME'}').query({ file: '../../../etc/passwd' });
124
+ expect(res.status).toBeGreaterThanOrEqual(400);
125
+ expect(res.text).not.toMatch(/^root:/m);
126
+ });
127
+ });`,
128
+ remediationSummary: () =>
129
+ `Canonicalize the resolved path via Path.GetFullPath / realpath, verify it starts with the allowed base directory, and reject mismatches. The bare Path.Combine / os.path.join does NOT prevent ../ escapes.`,
130
+ },
131
+
132
+ 'open-redirect': {
133
+ pocCurl: (f) => `# PoC: Open Redirect at ${f.file}:${f.line}
134
+ curl -I '${f._inRoute?.path || '/UPDATE_ME'}?${(f._sourceParam || 'url')}=//evil.example.com/phish'
135
+ # Expected vulnerable behavior: 302 Location: //evil.example.com/phish (attacker host)
136
+ # Expected fixed behavior: 4xx or redirect to same-origin only`,
137
+ regressionTestJest: (f) => `import request from 'supertest';
138
+ import app from '../app';
139
+ describe('${f.vuln}', () => {
140
+ it('rejects off-origin redirect targets', async () => {
141
+ const res = await request(app).get('${f._inRoute?.path || '/UPDATE_ME'}').query({ url: '//evil.example.com/phish' });
142
+ if (res.status === 302 || res.status === 301) {
143
+ expect(res.headers.location).not.toContain('evil.example.com');
144
+ }
145
+ });
146
+ });`,
147
+ remediationSummary: () =>
148
+ `Use Url.IsLocalUrl(url) (ASP.NET Core) / matching an allow-list of paths/hosts. Pass-through redirects to user-controlled URLs are the basis for OAuth pivot phishing.`,
149
+ },
150
+ };
151
+
152
+ /**
153
+ * Generate the 4-piece bundle for a finding. Returns:
154
+ * { family, pocs: { curl, ... }, tests: { jest, pytest, junit },
155
+ * remediation: { summary, diff }, exploitable: true|false|unknown }
156
+ *
157
+ * When the family has no template, returns a "stub" bundle with the
158
+ * available context so a downstream LLM can fill in the gaps.
159
+ */
160
+ export function generateBundle(finding, opts = {}) {
161
+ const family = finding.family || 'unknown';
162
+ const tmpl = TEMPLATES[family];
163
+ if (!tmpl) {
164
+ return {
165
+ family, exploitable: 'unknown',
166
+ pocs: { curl: null }, tests: { jest: null, pytest: null },
167
+ remediation: { summary: 'no template for this family yet — manual review required' },
168
+ stub: true,
169
+ };
170
+ }
171
+ const bundle = {
172
+ family,
173
+ cwe: finding.cwe,
174
+ sink: { file: finding.file, line: finding.line },
175
+ pocs: {
176
+ curl: tmpl.pocCurl ? tmpl.pocCurl(finding) : null,
177
+ },
178
+ tests: {
179
+ jest: tmpl.regressionTestJest ? tmpl.regressionTestJest(finding) : null,
180
+ pytest: tmpl.regressionTestPytest ? tmpl.regressionTestPytest(finding) : null,
181
+ },
182
+ remediation: {
183
+ summary: tmpl.remediationSummary ? tmpl.remediationSummary(finding) : finding.remediation || null,
184
+ // Diff generation is best done by the existing security-fixer subagent
185
+ // (it has full IR/file context). We emit only the summary here.
186
+ diff: null,
187
+ },
188
+ exploitable: 'pending-validation',
189
+ };
190
+ return bundle;
191
+ }
192
+
193
+ /**
194
+ * Bulk-emit bundles for an array of findings. Returns a Map<finding.id, bundle>.
195
+ * Caps at maxBundles for memory; sorted by severity DESC + compositeRisk DESC.
196
+ */
197
+ export function generateBundles(findings, opts = {}) {
198
+ if (!Array.isArray(findings)) return new Map();
199
+ const max = opts.maxBundles || 50;
200
+ const sev = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
201
+ const sorted = [...findings]
202
+ .filter(f => f.severity === 'critical' || f.severity === 'high')
203
+ .sort((a, b) => (sev[a.severity] - sev[b.severity]) || ((b.compositeRisk || 0) - (a.compositeRisk || 0)))
204
+ .slice(0, max);
205
+ const out = new Map();
206
+ for (const f of sorted) out.set(f.id || `${f.file}:${f.line}:${f.vuln}`, generateBundle(f, opts));
207
+ return out;
208
+ }
209
+
210
+ export const _internals = { TEMPLATES };
@@ -0,0 +1,172 @@
1
+ // Federated learning across opt-in customers — Recommendation #7 of the
2
+ // world-class+2 plan.
3
+ //
4
+ // Extends scanner/src/posture/triage-learning.js with a privacy-preserving
5
+ // cross-customer aggregation layer. Each opt-in customer's triage
6
+ // decisions contribute a noisy gradient (ε-differential privacy) to a
7
+ // central coordinator. The aggregated global prior gets blended with
8
+ // each local prior so all customers benefit from each other's calibration
9
+ // without sharing source code or findings.
10
+ //
11
+ // Privacy model:
12
+ // - We only submit (family, sink-method, tp_delta, fp_delta) — never
13
+ // the finding text, file path, or any identifier
14
+ // - Counts are perturbed with Laplace noise at scale 1/ε (default ε=1.0)
15
+ // - Receipts of every transmission written to
16
+ // .agentic-security/federated-receipts.jsonl for audit
17
+ // - Opt-in via AGENTIC_SECURITY_FEDERATED=1; off-by-default
18
+ //
19
+ // Threat model:
20
+ // - Attacker controls the coordinator → cannot recover an individual
21
+ // customer's triage history because of DP noise
22
+ // - Attacker controls one customer → can attempt poisoning, but each
23
+ // customer's contribution is capped via SUBMISSION_CAP_PER_DAY
24
+ // - Network observer → all traffic is HTTPS; payloads are aggregated
25
+ // counts not individual events
26
+
27
+ import * as fs from 'node:fs';
28
+ import * as path from 'node:path';
29
+ import * as crypto from 'node:crypto';
30
+ import { statePath, safeWriteState } from './state-dir.js';
31
+ import { loadCalibration } from './triage-learning.js';
32
+
33
+ const RECEIPTS_FILE = 'federated-receipts.jsonl';
34
+ const LAST_PUSH_FILE = 'federated-last-push.json';
35
+ const DEFAULT_EPSILON = 1.0;
36
+ const SUBMISSION_CAP_PER_DAY = 10;
37
+ const DEFAULT_PUSH_INTERVAL_HOURS = 24;
38
+
39
+ /**
40
+ * Sample from Laplace(0, b) where b = 1/ε. Used to add DP noise to count
41
+ * deltas before submission.
42
+ */
43
+ function laplaceNoise(epsilon) {
44
+ if (epsilon <= 0) return 0;
45
+ const u = Math.random() - 0.5;
46
+ const b = 1 / epsilon;
47
+ return -b * Math.sign(u) * Math.log(1 - 2 * Math.abs(u));
48
+ }
49
+
50
+ function _receiptsPath(scanRoot) { return statePath(scanRoot, RECEIPTS_FILE); }
51
+ function _lastPushPath(scanRoot) { return statePath(scanRoot, LAST_PUSH_FILE); }
52
+
53
+ function _appendReceipt(scanRoot, receipt) {
54
+ const fp = _receiptsPath(scanRoot);
55
+ try { fs.appendFileSync(fp, JSON.stringify(receipt) + '\n'); } catch {}
56
+ }
57
+
58
+ function _readLastPush(scanRoot) {
59
+ const fp = _lastPushPath(scanRoot);
60
+ if (!fs.existsSync(fp)) return null;
61
+ try { return JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return null; }
62
+ }
63
+
64
+ function _writeLastPush(scanRoot, payload) {
65
+ const fp = _lastPushPath(scanRoot);
66
+ safeWriteState(fp, JSON.stringify(payload, null, 2));
67
+ }
68
+
69
+ /**
70
+ * Compute the privatized gradient against a baseline calibration. The
71
+ * gradient is the per-bucket count delta since the last push, with
72
+ * Laplace noise added. The baseline is `lastPush.snapshot`; current
73
+ * counts come from the local triage-learning store.
74
+ */
75
+ export function computePrivatizedGradient(currentCalibration, baseline, opts = {}) {
76
+ const epsilon = opts.epsilon ?? DEFAULT_EPSILON;
77
+ const grad = { global: {}, perProject: null }; // perProject NEVER shared
78
+ const baseGlobal = baseline?.snapshot?.global || {};
79
+ for (const [bucket, cur] of Object.entries(currentCalibration.global || {})) {
80
+ const prev = baseGlobal[bucket] || { tp: 0, fp: 0 };
81
+ const tpDelta = (cur.tp || 0) - (prev.tp || 0);
82
+ const fpDelta = (cur.fp || 0) - (prev.fp || 0);
83
+ if (tpDelta === 0 && fpDelta === 0) continue;
84
+ grad.global[bucket] = {
85
+ tp: Math.max(0, Math.round(tpDelta + laplaceNoise(epsilon))),
86
+ fp: Math.max(0, Math.round(fpDelta + laplaceNoise(epsilon))),
87
+ };
88
+ }
89
+ return grad;
90
+ }
91
+
92
+ /**
93
+ * Push the privatized gradient to the central coordinator. Coordinator
94
+ * endpoint defaults to a known address; can be overridden by
95
+ * AGENTIC_SECURITY_FEDERATED_ENDPOINT. Records a receipt on success or
96
+ * failure.
97
+ */
98
+ export async function pushGradient(scanRoot, gradient, opts = {}) {
99
+ if (process.env.AGENTIC_SECURITY_FEDERATED !== '1') {
100
+ return { ok: false, reason: 'opt-in-not-enabled' };
101
+ }
102
+ const last = _readLastPush(scanRoot) || { count: 0, day: '' };
103
+ const today = new Date().toISOString().slice(0, 10);
104
+ if (last.day === today && last.count >= SUBMISSION_CAP_PER_DAY) {
105
+ return { ok: false, reason: 'daily-cap-reached', cap: SUBMISSION_CAP_PER_DAY };
106
+ }
107
+ const endpoint = opts.endpoint || process.env.AGENTIC_SECURITY_FEDERATED_ENDPOINT;
108
+ if (!endpoint) return { ok: false, reason: 'no-endpoint-configured' };
109
+ const payload = {
110
+ schema: 'agentic-security/federated-grad/v1',
111
+ epsilon: opts.epsilon ?? DEFAULT_EPSILON,
112
+ bucketCount: Object.keys(gradient.global || {}).length,
113
+ gradient: gradient.global || {},
114
+ ts: new Date().toISOString(),
115
+ submissionId: crypto.randomBytes(8).toString('hex'),
116
+ };
117
+ try {
118
+ const res = await fetch(endpoint, {
119
+ method: 'POST',
120
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'agentic-security/0.1' },
121
+ body: JSON.stringify(payload),
122
+ });
123
+ const ok = res.ok;
124
+ const status = res.status;
125
+ _appendReceipt(scanRoot, { ts: payload.ts, submissionId: payload.submissionId, ok, status, bucketCount: payload.bucketCount });
126
+ _writeLastPush(scanRoot, {
127
+ count: (last.day === today ? last.count : 0) + 1, day: today,
128
+ snapshot: { global: Object.fromEntries(Object.entries(loadCalibration(scanRoot).global || {})) },
129
+ });
130
+ return { ok, status, submissionId: payload.submissionId };
131
+ } catch (e) {
132
+ _appendReceipt(scanRoot, { ts: payload.ts, submissionId: payload.submissionId, ok: false, error: String(e && e.message || e) });
133
+ return { ok: false, reason: 'network-error', error: String(e && e.message || e) };
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Pull the aggregated global prior from the coordinator. The coordinator
139
+ * returns an aggregated calibration after combining all opt-in customer
140
+ * gradients.
141
+ */
142
+ export async function pullAggregatedPrior(scanRoot, opts = {}) {
143
+ if (process.env.AGENTIC_SECURITY_FEDERATED !== '1') return null;
144
+ const endpoint = (opts.endpoint || process.env.AGENTIC_SECURITY_FEDERATED_ENDPOINT || '').replace(/\/$/, '');
145
+ if (!endpoint) return null;
146
+ try {
147
+ const res = await fetch(`${endpoint}/aggregate`, {
148
+ headers: { 'User-Agent': 'agentic-security/0.1' },
149
+ });
150
+ if (!res.ok) return null;
151
+ const prior = await res.json();
152
+ _appendReceipt(scanRoot, { ts: new Date().toISOString(), kind: 'pull', ok: true });
153
+ return prior;
154
+ } catch { return null; }
155
+ }
156
+
157
+ /**
158
+ * Run the periodic federated learning cycle: compute privatized
159
+ * gradient against last push baseline, submit, fetch aggregated prior.
160
+ */
161
+ export async function federatedCycle(scanRoot, opts = {}) {
162
+ const calibration = loadCalibration(scanRoot);
163
+ const last = _readLastPush(scanRoot);
164
+ const gradient = computePrivatizedGradient(calibration, last || {}, opts);
165
+ const pushResult = await pushGradient(scanRoot, gradient, opts);
166
+ const pullResult = await pullAggregatedPrior(scanRoot, opts);
167
+ return { gradient, pushResult, aggregatedPrior: pullResult };
168
+ }
169
+
170
+ export const _internals = {
171
+ laplaceNoise, DEFAULT_EPSILON, SUBMISSION_CAP_PER_DAY, DEFAULT_PUSH_INTERVAL_HOURS,
172
+ };
@@ -0,0 +1,94 @@
1
+ // Attributions emitter — companion to license-graph.js.
2
+ //
3
+ // Generates two artifacts under .agentic-security/:
4
+ //
5
+ // ATTRIBUTIONS.md — Markdown table of every component with its
6
+ // license, version, copyright holder (when known),
7
+ // and source URL.
8
+ // NOTICE — Apache-style NOTICE file (only when Apache-2.0
9
+ // licensed components are present).
10
+ //
11
+ // The emitter is deterministic — sorts by ecosystem, name, version —
12
+ // so commits don't churn between scans.
13
+
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+
17
+ function _sortKey(c) {
18
+ return `${c.ecosystem || 'zz'}:${c.name || ''}:${c.version || ''}`;
19
+ }
20
+
21
+ function _safeStr(s) { return String(s || '').replace(/\s+/g, ' ').trim(); }
22
+
23
+ function _inferRepoUrl(c) {
24
+ if (c.repository?.url) return c.repository.url;
25
+ if (typeof c.repository === 'string') return c.repository;
26
+ if (c.homepage) return c.homepage;
27
+ // Common conventions for ecosystem package pages.
28
+ switch (c.ecosystem) {
29
+ case 'npm': return `https://www.npmjs.com/package/${c.name}`;
30
+ case 'pypi': return `https://pypi.org/project/${c.name}/`;
31
+ case 'rubygems': return `https://rubygems.org/gems/${c.name}`;
32
+ case 'cargo': return `https://crates.io/crates/${c.name}`;
33
+ case 'maven': return `https://search.maven.org/artifact/${(c.name || '').replace(':', '/')}`;
34
+ case 'packagist': return `https://packagist.org/packages/${c.name}`;
35
+ case 'pub': return `https://pub.dev/packages/${c.name}`;
36
+ case 'golang': return `https://pkg.go.dev/${c.name}`;
37
+ default: return '';
38
+ }
39
+ }
40
+
41
+ export function generateAttributions(components, options) {
42
+ const opts = options || {};
43
+ const list = (components || []).slice().sort((a, b) => _sortKey(a).localeCompare(_sortKey(b)));
44
+ if (!list.length) return { markdown: '', notice: '' };
45
+
46
+ const lines = [];
47
+ lines.push('# Third-party attributions');
48
+ lines.push('');
49
+ lines.push(`Generated by agentic-security on ${new Date().toISOString().slice(0, 10)}.`);
50
+ lines.push('');
51
+ lines.push(`This project incorporates **${list.length}** third-party components. Each is listed with its license and source URL. See the linked source for the full license text.`);
52
+ lines.push('');
53
+ lines.push('| Package | Version | License | Ecosystem | Source |');
54
+ lines.push('|---------|---------|---------|-----------|--------|');
55
+ for (const c of list) {
56
+ const url = _inferRepoUrl(c);
57
+ const urlMd = url ? `[link](${url})` : '—';
58
+ lines.push(`| ${_safeStr(c.name)} | ${_safeStr(c.version)} | ${_safeStr(c.license || '—')} | ${_safeStr(c.ecosystem)} | ${urlMd} |`);
59
+ }
60
+ lines.push('');
61
+ const markdown = lines.join('\n');
62
+
63
+ // Apache NOTICE: include only Apache-2.0 components per § 4(d) requirement.
64
+ const apache = list.filter(c => /APACHE-2\.0/i.test(c.license || ''));
65
+ let notice = '';
66
+ if (apache.length) {
67
+ const nl = [];
68
+ nl.push(`${opts.projectName || 'This product'} includes software developed by third parties under the Apache 2.0 license:`);
69
+ nl.push('');
70
+ for (const c of apache) {
71
+ nl.push(`* ${c.name} (${c.version || 'unknown version'})`);
72
+ if (c.copyright) nl.push(` Copyright: ${_safeStr(c.copyright)}`);
73
+ const url = _inferRepoUrl(c);
74
+ if (url) nl.push(` Source: ${url}`);
75
+ }
76
+ nl.push('');
77
+ nl.push('See LICENSE / ATTRIBUTIONS.md for full license text.');
78
+ notice = nl.join('\n');
79
+ }
80
+
81
+ return { markdown, notice, componentCount: list.length };
82
+ }
83
+
84
+ export function persistAttributions(scanRoot, result) {
85
+ if (!result || !result.markdown) return null;
86
+ try { fs.mkdirSync(path.join(scanRoot, '.agentic-security'), { recursive: true }); } catch {}
87
+ try { fs.writeFileSync(path.join(scanRoot, '.agentic-security', 'ATTRIBUTIONS.md'), result.markdown); } catch {}
88
+ if (result.notice) {
89
+ try { fs.writeFileSync(path.join(scanRoot, '.agentic-security', 'NOTICE'), result.notice); } catch {}
90
+ }
91
+ return result;
92
+ }
93
+
94
+ export const _internals = { _inferRepoUrl, _sortKey, _safeStr };