@clear-capabilities/agentic-security-scanner 0.172.0 → 0.173.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.
- package/CHANGELOG.md +29 -0
- package/bin/agentic-security.js +37 -0
- package/dist/119.index.js +197 -0
- package/dist/128.index.js +129 -0
- package/dist/1293.index.js +5689 -0
- package/dist/1336.index.js +101 -101
- package/dist/1351.index.js +133 -0
- package/dist/1387.index.js +271 -0
- package/dist/1437.index.js +23 -0
- package/dist/1445.index.js +110 -0
- package/dist/1505.index.js +151 -0
- package/dist/1557.index.js +293 -0
- package/dist/1621.index.js +311 -0
- package/dist/1751.index.js +160 -0
- package/dist/1765.index.js +727 -0
- package/dist/1861.index.js +690 -0
- package/dist/1898.index.js +116 -0
- package/dist/1906.index.js +433 -0
- package/dist/1920.index.js +1 -291
- package/dist/208.index.js +1631 -0
- package/dist/2199.index.js +0 -239
- package/dist/2223.index.js +159 -0
- package/dist/2530.index.js +152 -0
- package/dist/2565.index.js +116 -0
- package/dist/2574.index.js +127 -0
- package/dist/2620.index.js +345 -0
- package/dist/2773.index.js +98 -0
- package/dist/2775.index.js +32 -0
- package/dist/2966.index.js +171 -0
- package/dist/2972.index.js +87 -0
- package/dist/2991.index.js +131 -0
- package/dist/3063.index.js +833 -0
- package/dist/3075.index.js +227 -0
- package/dist/3121.index.js +297 -0
- package/dist/3314.index.js +263 -0
- package/dist/3480.index.js +1822 -0
- package/dist/3481.index.js +24 -0
- package/dist/3506.index.js +268 -0
- package/dist/3533.index.js +591 -0
- package/dist/3653.index.js +450 -0
- package/dist/3822.index.js +131 -0
- package/dist/3868.index.js +292 -0
- package/dist/3917.index.js +152 -0
- package/dist/3918.index.js +192 -0
- package/dist/4090.index.js +288 -0
- package/dist/4121.index.js +1190 -0
- package/dist/4142.index.js +0 -239
- package/dist/4336.index.js +436 -0
- package/dist/4387.index.js +532 -0
- package/dist/4413.index.js +302 -0
- package/dist/4476.index.js +4510 -0
- package/dist/4491.index.js +133 -0
- package/dist/4538.index.js +185 -0
- package/dist/4684.index.js +319 -0
- package/dist/4740.index.js +165 -0
- package/dist/4780.index.js +209 -0
- package/dist/4802.index.js +225 -0
- package/dist/4812.index.js +379 -0
- package/dist/4835.index.js +126 -0
- package/dist/494.index.js +226 -0
- package/dist/4991.index.js +549 -0
- package/dist/5199.index.js +305 -0
- package/dist/5216.index.js +109 -0
- package/dist/5439.index.js +301 -0
- package/dist/5443.index.js +97 -0
- package/dist/5496.index.js +377 -0
- package/dist/550.index.js +2 -1
- package/dist/5801.index.js +588 -0
- package/dist/5858.index.js +223 -0
- package/dist/5933.index.js +143 -0
- package/dist/601.index.js +23 -0
- package/dist/6173.index.js +273 -0
- package/dist/6291.index.js +1871 -0
- package/dist/6312.index.js +666 -0
- package/dist/634.index.js +16182 -0
- package/dist/644.index.js +87 -0
- package/dist/6508.index.js +283 -0
- package/dist/6560.index.js +645 -0
- package/dist/6576.index.js +422 -0
- package/dist/6579.index.js +271 -0
- package/dist/6745.index.js +305 -0
- package/dist/6818.index.js +21642 -0
- package/dist/6972.index.js +167 -0
- package/dist/6995.index.js +163 -0
- package/dist/7249.index.js +191 -0
- package/dist/7372.index.js +1197 -0
- package/dist/7405.index.js +23 -6
- package/dist/7546.index.js +541 -0
- package/dist/76.index.js +448 -0
- package/dist/7879.index.js +163 -0
- package/dist/8025.index.js +130 -0
- package/dist/8054.index.js +248 -0
- package/dist/8097.index.js +201 -0
- package/dist/8126.index.js +191 -0
- package/dist/8178.index.js +78 -0
- package/dist/818.index.js +212 -0
- package/dist/8382.index.js +93 -0
- package/dist/8408.index.js +261 -0
- package/dist/8540.index.js +174 -0
- package/dist/8635.index.js +802 -0
- package/dist/8688.index.js +410 -0
- package/dist/8699.index.js +779 -0
- package/dist/8891.index.js +457 -0
- package/dist/8916.index.js +885 -0
- package/dist/9146.index.js +170 -0
- package/dist/9168.index.js +841 -0
- package/dist/9292.index.js +369 -0
- package/dist/9382.index.js +338 -48
- package/dist/9422.index.js +866 -0
- package/dist/9440.index.js +453 -0
- package/dist/9479.index.js +152 -0
- package/dist/9481.index.js +220 -0
- package/dist/9539.index.js +100 -0
- package/dist/9547.index.js +103 -0
- package/dist/9560.index.js +41 -61
- package/dist/9692.index.js +90 -0
- package/dist/9800.index.js +213 -1
- package/dist/9899.index.js +138 -0
- package/dist/9981.index.js +206 -0
- package/dist/999.index.js +117 -0
- package/dist/agentic-security.mjs +28 -28
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +31 -6
- package/src/context-intelligence/CLAUDE.md +4 -0
- package/src/context-intelligence/adapters/haskell-hie.js +28 -1
- package/src/context-intelligence/candidates.js +2 -1
- package/src/context-intelligence/canonical.js +25 -0
- package/src/context-intelligence/capabilities.js +2 -1
- package/src/context-intelligence/cli.js +16 -10
- package/src/context-intelligence/config.js +4 -1
- package/src/context-intelligence/controls.js +108 -10
- package/src/context-intelligence/counterexample.js +200 -0
- package/src/context-intelligence/cross-route.js +111 -0
- package/src/context-intelligence/entrypoints.js +2 -2
- package/src/context-intelligence/evaluation/index.js +11 -4
- package/src/context-intelligence/evaluation/scripted-reader.js +12 -2
- package/src/context-intelligence/findings.js +166 -0
- package/src/context-intelligence/invariants.js +197 -0
- package/src/context-intelligence/queue.js +6 -1
- package/src/context-intelligence/reachability.js +10 -4
- package/src/context-intelligence/review.js +129 -16
- package/src/dataflow/access-paths.js +11 -0
- package/src/dataflow/analysis-limits.js +66 -0
- package/src/dataflow/engine.js +272 -20
- package/src/dataflow/service-edges.js +174 -0
- package/src/dataflow/summaries.js +7 -1
- package/src/discovery/judge.js +2 -1
- package/src/engine.js +82 -14
- package/src/fix/apply-fix-service.js +55 -8
- package/src/fix/batch-journal.js +84 -0
- package/src/fix/exploit-replay.js +51 -0
- package/src/fix/footprint.js +65 -0
- package/src/fix/patch-policy.js +49 -0
- package/src/fix/secret-persistence.js +39 -0
- package/src/fix/test-commands.js +84 -0
- package/src/fix/verification-adapters.js +97 -0
- package/src/fix/verification-record.js +243 -0
- package/src/fix/verify-patch.js +89 -0
- package/src/ir/CLAUDE.md +5 -0
- package/src/ir/callgraph.js +23 -1
- package/src/ir/module-resolver.js +315 -0
- package/src/ir/parser-js.js +21 -2
- package/src/ir/semantic-graph.js +245 -0
- package/src/lineage/build-attested-correlation.js +63 -0
- package/src/lineage/deployment-identity.js +76 -0
- package/src/mcp/tools.js +13 -34
- package/src/pipeline/canonical-output.js +64 -0
- package/src/pipeline/detector-runner.js +7 -0
- package/src/pipeline/scan-health.js +14 -0
- package/src/pipeline/supervised-analyzers.js +125 -0
- package/src/pipeline/supervised-child.js +17 -0
- package/src/pipeline/telemetry.js +79 -0
- package/src/posture/accuracy-scorecard.js +98 -9
- package/src/posture/artifact-registry.js +5 -0
- package/src/posture/assurance/evidence-registry.js +215 -0
- package/src/posture/assurance/release-package-verify.js +135 -0
- package/src/posture/assurance/release-package.js +118 -0
- package/src/posture/assurance/release-verdict.js +94 -0
- package/src/posture/assurance/waiver.js +200 -0
- package/src/posture/compliance-truth.js +100 -0
- package/src/posture/evaluation/scan-child.js +3 -1
- package/src/posture/evaluation/why-missed.js +3 -1
- package/src/posture/exploitability-evidence.js +141 -0
- package/src/posture/fix-verify.js +4 -2
- package/src/posture/fleet/exposure-ranking.js +115 -0
- package/src/posture/fleet/service-chain-graph.js +150 -0
- package/src/posture/graph-reconciliation.js +154 -0
- package/src/posture/provenance/coverage.js +103 -0
- package/src/posture/provenance/git-evidence.js +84 -0
- package/src/posture/provenance/origin-resolver.js +7 -1
- package/src/posture/provenance/predicate-replay.js +14 -4
- package/src/posture/runtime-correlation.js +61 -15
- package/src/posture/runtime-explanation.js +116 -0
- package/src/posture/runtime-trace-ingest.js +194 -0
- package/src/posture/scan-checkpoint.js +38 -10
- package/src/posture/waf-ingest.js +7 -2
- package/src/report/index.js +4 -0
- package/src/report/oscal.js +38 -0
- package/src/report/perspectives.js +150 -0
- package/src/sast/_comment-strip.js +8 -1
- package/src/sast/ruby.js +26 -0
- package/src/util/string-memo.js +38 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
export const id = 6972;
|
|
2
|
+
export const ids = [6972];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 16972:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ describeTarget: () => (/* binding */ describeTarget),
|
|
10
|
+
/* harmony export */ loadTargetManifest: () => (/* binding */ loadTargetManifest),
|
|
11
|
+
/* harmony export */ validateTarget: () => (/* binding */ validateTarget)
|
|
12
|
+
/* harmony export */ });
|
|
13
|
+
/* unused harmony export _internals */
|
|
14
|
+
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
|
|
15
|
+
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(76760);
|
|
16
|
+
// Verifier target harness (FR-LIVE-HARNESS).
|
|
17
|
+
//
|
|
18
|
+
// Customer projects describe how to bring up the app they want the verifier
|
|
19
|
+
// to execute PoCs against. The manifest lives at
|
|
20
|
+
// `.agentic-security/verifier-target.yaml` and the verifier reads it before
|
|
21
|
+
// running `--live` mode. v1 supports two manifest shapes:
|
|
22
|
+
//
|
|
23
|
+
// shape: docker-compose
|
|
24
|
+
// compose: docker-compose.yml
|
|
25
|
+
// service: web
|
|
26
|
+
// port: 3000
|
|
27
|
+
// wait-for: http://localhost:3000/healthz
|
|
28
|
+
//
|
|
29
|
+
// ─── or ───
|
|
30
|
+
//
|
|
31
|
+
// shape: command
|
|
32
|
+
// start: npm run dev
|
|
33
|
+
// port: 3000
|
|
34
|
+
// wait-for: http://localhost:3000/healthz
|
|
35
|
+
// stop: pkill -f "npm run dev"
|
|
36
|
+
//
|
|
37
|
+
// We DO NOT execute the manifest in this module — that's the verifier
|
|
38
|
+
// sandbox's job (and the customer's explicit opt-in via --live). This
|
|
39
|
+
// module parses, validates, and surfaces a structured object the verifier
|
|
40
|
+
// can act on (or refuse to act on).
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
const MANIFEST_PATH = node_path__WEBPACK_IMPORTED_MODULE_1__.join('.agentic-security', 'verifier-target.yaml');
|
|
46
|
+
|
|
47
|
+
// Minimal YAML subset parser — keep parity with the polyglot bench parser,
|
|
48
|
+
// scoped to the small set of keys this manifest uses. Standalone here to
|
|
49
|
+
// avoid a runtime dep.
|
|
50
|
+
function _parse(text) {
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const raw of String(text || '').split(/\r?\n/)) {
|
|
53
|
+
const line = raw.replace(/#.*$/, '').trim();
|
|
54
|
+
if (!line) continue;
|
|
55
|
+
const m = line.match(/^([\w-]+)\s*:\s*(.*)$/);
|
|
56
|
+
if (!m) continue;
|
|
57
|
+
const [, key, val] = m;
|
|
58
|
+
out[key] = _coerce(val);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _coerce(v) {
|
|
64
|
+
if (v === undefined || v === null) return null;
|
|
65
|
+
v = String(v).trim();
|
|
66
|
+
if (/^".*"$/.test(v)) return v.slice(1, -1);
|
|
67
|
+
if (/^'.*'$/.test(v)) return v.slice(1, -1);
|
|
68
|
+
if (/^-?\d+$/.test(v)) return parseInt(v, 10);
|
|
69
|
+
return v;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Read the verifier-target manifest from scanRoot. Returns:
|
|
74
|
+
* { ok: true, target } — successfully parsed and validated
|
|
75
|
+
* { ok: false, reason } — manifest missing or invalid
|
|
76
|
+
*
|
|
77
|
+
* Never throws.
|
|
78
|
+
*/
|
|
79
|
+
function loadTargetManifest(scanRoot) {
|
|
80
|
+
const fp = node_path__WEBPACK_IMPORTED_MODULE_1__.join(scanRoot || process.cwd(), MANIFEST_PATH);
|
|
81
|
+
if (!node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(fp)) return { ok: false, reason: 'no-manifest' };
|
|
82
|
+
let raw;
|
|
83
|
+
try { raw = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(fp, 'utf8'); }
|
|
84
|
+
catch (e) { return { ok: false, reason: `read-error:${e.message}` }; }
|
|
85
|
+
const parsed = _parse(raw);
|
|
86
|
+
const shape = parsed.shape;
|
|
87
|
+
if (shape !== 'docker-compose' && shape !== 'command') {
|
|
88
|
+
return { ok: false, reason: `unknown-shape:${shape || 'missing'}` };
|
|
89
|
+
}
|
|
90
|
+
if (shape === 'docker-compose') {
|
|
91
|
+
if (!parsed.compose || !parsed.service) {
|
|
92
|
+
return { ok: false, reason: 'docker-compose-shape-needs-compose-and-service' };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (shape === 'command') {
|
|
96
|
+
if (!parsed.start) {
|
|
97
|
+
return { ok: false, reason: 'command-shape-needs-start' };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const target = {
|
|
101
|
+
shape,
|
|
102
|
+
compose: parsed.compose || null,
|
|
103
|
+
service: parsed.service || null,
|
|
104
|
+
start: parsed.start || null,
|
|
105
|
+
stop: parsed.stop || null,
|
|
106
|
+
port: parsed.port || null,
|
|
107
|
+
waitFor: parsed['wait-for'] || parsed.waitFor || null,
|
|
108
|
+
url: parsed.url || (parsed.port ? `http://localhost:${parsed.port}` : null),
|
|
109
|
+
};
|
|
110
|
+
return { ok: true, target };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Render a quick summary for human consumption (CLI output, logs).
|
|
115
|
+
*/
|
|
116
|
+
function describeTarget(target) {
|
|
117
|
+
if (!target) return '(none)';
|
|
118
|
+
if (target.shape === 'docker-compose') {
|
|
119
|
+
return `docker-compose service ${target.service} from ${target.compose} on ${target.url || `:${target.port}`}`;
|
|
120
|
+
}
|
|
121
|
+
if (target.shape === 'command') {
|
|
122
|
+
return `command "${target.start}" on ${target.url || `:${target.port}`}`;
|
|
123
|
+
}
|
|
124
|
+
return `unknown-shape ${target.shape}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Pre-flight safety check: refuse to bring up a target whose start command
|
|
129
|
+
* looks dangerous. Curated allowlist of common shapes; everything else
|
|
130
|
+
* requires AGENTIC_SECURITY_VERIFY_TARGET_OK=1 to opt in explicitly.
|
|
131
|
+
*/
|
|
132
|
+
function validateTarget(target) {
|
|
133
|
+
if (!target) return { ok: false, reason: 'no-target' };
|
|
134
|
+
if (target.shape === 'docker-compose') {
|
|
135
|
+
// docker-compose is safe-ish by design.
|
|
136
|
+
return { ok: true };
|
|
137
|
+
}
|
|
138
|
+
if (target.shape === 'command') {
|
|
139
|
+
const SAFE_HINTS = [
|
|
140
|
+
/^npm\s+(?:run\s+)?(?:dev|start|serve)/,
|
|
141
|
+
/^yarn\s+(?:dev|start|serve)/,
|
|
142
|
+
/^pnpm\s+(?:run\s+)?(?:dev|start|serve)/,
|
|
143
|
+
/^node\s+/,
|
|
144
|
+
/^python(?:\d?)\s+-m\s+/,
|
|
145
|
+
/^python(?:\d?)\s+\S+\.py/,
|
|
146
|
+
/^uvicorn\s+/,
|
|
147
|
+
/^gunicorn\s+/,
|
|
148
|
+
/^flask\s+run/,
|
|
149
|
+
/^go\s+run\s+/,
|
|
150
|
+
/^java\s+-jar\s+/,
|
|
151
|
+
/^cargo\s+run/,
|
|
152
|
+
];
|
|
153
|
+
if (process.env.AGENTIC_SECURITY_VERIFY_TARGET_OK === '1') return { ok: true, escaped: true };
|
|
154
|
+
if (!SAFE_HINTS.some(re => re.test(target.start))) {
|
|
155
|
+
return { ok: false, reason: `start-command-not-in-safe-allowlist (set AGENTIC_SECURITY_VERIFY_TARGET_OK=1 to override)` };
|
|
156
|
+
}
|
|
157
|
+
return { ok: true };
|
|
158
|
+
}
|
|
159
|
+
return { ok: false, reason: `unknown-shape:${target.shape}` };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const _internals = { MANIFEST_PATH };
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
/***/ })
|
|
166
|
+
|
|
167
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
export const id = 6995;
|
|
2
|
+
export const ids = [6995];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 66995:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ addLegalHold: () => (/* binding */ addLegalHold),
|
|
10
|
+
/* harmony export */ isUnderHold: () => (/* binding */ isUnderHold),
|
|
11
|
+
/* harmony export */ listLegalHolds: () => (/* binding */ listLegalHolds),
|
|
12
|
+
/* harmony export */ loadLegalHolds: () => (/* binding */ loadLegalHolds),
|
|
13
|
+
/* harmony export */ removeLegalHold: () => (/* binding */ removeLegalHold)
|
|
14
|
+
/* harmony export */ });
|
|
15
|
+
/* unused harmony export LEGAL_HOLD_FILE */
|
|
16
|
+
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
|
|
17
|
+
/* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(69259);
|
|
18
|
+
/* harmony import */ var _artifact_registry_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(81734);
|
|
19
|
+
// FR-707 (assurance-hardening PRD): "Support legal hold and policy-
|
|
20
|
+
// authorized retention exceptions | Legal hold is identity-bound, reasoned,
|
|
21
|
+
// time-bounded where applicable, and auditable."
|
|
22
|
+
//
|
|
23
|
+
// A third instance of the recurring {owner, reason, expires_at} exception
|
|
24
|
+
// shape this codebase already uses twice — `posture/suppressions.js`'s
|
|
25
|
+
// pro-tier exception (scoped to a FINDING) and `posture/compliance-policy.js`'s
|
|
26
|
+
// structured `not-applicable` (scoped to a COMPLIANCE CONTROL). Per D-0025,
|
|
27
|
+
// these are deliberately distinct mechanisms serving different subjects, not
|
|
28
|
+
// one shared module — this file is the third subject: a STATE ARTIFACT.
|
|
29
|
+
//
|
|
30
|
+
// Field naming matches the existing two schemas' snake_case convention
|
|
31
|
+
// (`owner`, `reason`, `expires_at`) rather than inventing a fourth style.
|
|
32
|
+
//
|
|
33
|
+
// identity-bound -> `owner` (required, who placed the hold and is
|
|
34
|
+
// accountable for lifting it)
|
|
35
|
+
// reasoned -> `reason` (required — "we might need this later" is
|
|
36
|
+
// not a reason; same discipline suppressions.js
|
|
37
|
+
// already enforces for its own exceptions)
|
|
38
|
+
// time-bounded
|
|
39
|
+
// where applicable -> `expires_at` is OPTIONAL: a null/absent value is
|
|
40
|
+
// an INDEFINITE hold, which the acceptance
|
|
41
|
+
// criterion's own "where applicable" phrase
|
|
42
|
+
// explicitly allows (a genuine legal matter may have
|
|
43
|
+
// no known end date) — an ISO date value behaves
|
|
44
|
+
// exactly like FR-506/FR-1004's own expiring
|
|
45
|
+
// exceptions: once past, the hold is no longer
|
|
46
|
+
// active and the artifact is exposed to its normal
|
|
47
|
+
// retention TTL again.
|
|
48
|
+
// auditable -> persisted as a single JSON array under
|
|
49
|
+
// `.agentic-security/legal-holds.json` (itself
|
|
50
|
+
// registered as operator-config — an operator/legal
|
|
51
|
+
// team's own input, never scanner-written from scan
|
|
52
|
+
// results), readable via `listLegalHolds`.
|
|
53
|
+
//
|
|
54
|
+
// Consulted from TWO places, not one: `retention-policy.js#findExpiredArtifacts`
|
|
55
|
+
// (defense in depth for any caller reaching it directly) AND `cmdReset`
|
|
56
|
+
// itself for its PLAIN (non-`--expired`) path, which deletes every
|
|
57
|
+
// registered 'generated' artifact unconditionally and would otherwise blow
|
|
58
|
+
// through a hold that only gated TTL expiry.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
const LEGAL_HOLD_FILE = 'legal-holds.json';
|
|
65
|
+
|
|
66
|
+
function _loadRaw(scanRoot) {
|
|
67
|
+
let fp;
|
|
68
|
+
try { fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__.statePath)(scanRoot, LEGAL_HOLD_FILE); } catch { return []; }
|
|
69
|
+
let raw;
|
|
70
|
+
try { raw = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(fp, 'utf8'); } catch { return []; }
|
|
71
|
+
try {
|
|
72
|
+
const doc = JSON.parse(raw);
|
|
73
|
+
return Array.isArray(doc) ? doc : [];
|
|
74
|
+
} catch { return []; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* All legal holds ever recorded for this project, malformed entries
|
|
79
|
+
* dropped rather than throwing. Never filters by expiry — see
|
|
80
|
+
* `isUnderHold`/`listLegalHolds` for that.
|
|
81
|
+
*/
|
|
82
|
+
function loadLegalHolds(scanRoot) {
|
|
83
|
+
return _loadRaw(scanRoot).filter(h => h && typeof h === 'object' && typeof h.artifact === 'string' && typeof h.owner === 'string' && typeof h.reason === 'string');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Is `artifactName` currently protected by an active (non-expired) hold?
|
|
88
|
+
* Returns the matching hold record, or null. Multiple holds on the same
|
|
89
|
+
* artifact are permitted (e.g. two independent legal matters); the first
|
|
90
|
+
* still-active one found is returned.
|
|
91
|
+
*/
|
|
92
|
+
function isUnderHold(artifactName, holds, now = Date.now()) {
|
|
93
|
+
for (const h of holds) {
|
|
94
|
+
if (h.artifact !== artifactName) continue;
|
|
95
|
+
if (!h.expires_at) return h; // indefinite hold — always active
|
|
96
|
+
const t = Date.parse(h.expires_at);
|
|
97
|
+
if (!Number.isFinite(t) || t >= now) return h;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Active (non-expired) holds only, unless `includeExpired`. For
|
|
104
|
+
* `legal-hold list` / auditing.
|
|
105
|
+
*/
|
|
106
|
+
function listLegalHolds(scanRoot, { includeExpired = false, now = Date.now() } = {}) {
|
|
107
|
+
const holds = loadLegalHolds(scanRoot);
|
|
108
|
+
if (includeExpired) return holds;
|
|
109
|
+
return holds.filter(h => {
|
|
110
|
+
if (!h.expires_at) return true;
|
|
111
|
+
const t = Date.parse(h.expires_at);
|
|
112
|
+
return !Number.isFinite(t) || t >= now;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Add a legal hold. Validates identity-bound + reasoned up front (both
|
|
118
|
+
* required, non-empty) and that `expires_at`, if given, is a parseable
|
|
119
|
+
* date in the future — an already-expired hold would be a hold that
|
|
120
|
+
* protects nothing, which is never a legitimate request. `artifact` must
|
|
121
|
+
* name a real registered artifact (artifact-registry.js) — a hold on an
|
|
122
|
+
* unrecognised name can never protect anything and almost always means a
|
|
123
|
+
* typo. Returns `{ok:true, hold}` or `{ok:false, reason}`; never throws.
|
|
124
|
+
*/
|
|
125
|
+
function addLegalHold(scanRoot, { artifact, owner, reason, expires_at } = {}) {
|
|
126
|
+
if (!artifact || typeof artifact !== 'string') return { ok: false, reason: '--artifact is required' };
|
|
127
|
+
if (!(0,_artifact_registry_js__WEBPACK_IMPORTED_MODULE_2__/* .isRegisteredArtifact */ .Jl)(artifact)) return { ok: false, reason: `"${artifact}" is not a registered state artifact` };
|
|
128
|
+
if (!owner || typeof owner !== 'string') return { ok: false, reason: '--owner is required (identity-bound)' };
|
|
129
|
+
if (!reason || typeof reason !== 'string') return { ok: false, reason: '--reason is required (reasoned)' };
|
|
130
|
+
if (expires_at) {
|
|
131
|
+
const t = Date.parse(expires_at);
|
|
132
|
+
if (!Number.isFinite(t)) return { ok: false, reason: 'expires_at must be a parseable date' };
|
|
133
|
+
if (t < Date.now()) return { ok: false, reason: 'expires_at is in the past — a hold that already expired protects nothing' };
|
|
134
|
+
}
|
|
135
|
+
const hold = { artifact, owner, reason, expires_at: expires_at || null, created_at: new Date().toISOString() };
|
|
136
|
+
const holds = _loadRaw(scanRoot);
|
|
137
|
+
holds.push(hold);
|
|
138
|
+
const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__.statePath)(scanRoot, LEGAL_HOLD_FILE);
|
|
139
|
+
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__.safeWriteState)(fp, JSON.stringify(holds, null, 2) + '\n')) {
|
|
140
|
+
return { ok: false, reason: 'state writes are disabled (--no-state) or this is not a safe state directory' };
|
|
141
|
+
}
|
|
142
|
+
return { ok: true, hold };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Remove every hold on `artifact` (lifting a hold, not letting it expire).
|
|
147
|
+
* Returns the number removed. A no-op (0) if none existed — never an error.
|
|
148
|
+
*/
|
|
149
|
+
function removeLegalHold(scanRoot, artifact) {
|
|
150
|
+
const holds = _loadRaw(scanRoot);
|
|
151
|
+
const remaining = holds.filter(h => !(h && h.artifact === artifact));
|
|
152
|
+
const removedCount = holds.length - remaining.length;
|
|
153
|
+
if (removedCount > 0) {
|
|
154
|
+
const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__.statePath)(scanRoot, LEGAL_HOLD_FILE);
|
|
155
|
+
(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__.safeWriteState)(fp, JSON.stringify(remaining, null, 2) + '\n');
|
|
156
|
+
}
|
|
157
|
+
return removedCount;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
/***/ })
|
|
162
|
+
|
|
163
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
export const id = 7249;
|
|
2
|
+
export const ids = [7249];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 27249:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ renderPrComment: () => (/* binding */ renderPrComment)
|
|
10
|
+
/* harmony export */ });
|
|
11
|
+
/* unused harmony export _internal */
|
|
12
|
+
/* harmony import */ var _util_untrusted_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(11274);
|
|
13
|
+
// Advisor-tone PR comment renderer (v0.72).
|
|
14
|
+
//
|
|
15
|
+
// Replaces the typical "12 findings detected, see SARIF" wall of text
|
|
16
|
+
// with a single security-advisor's note:
|
|
17
|
+
//
|
|
18
|
+
// "I noticed you added /api/admin/users in this PR. I checked 4 things:
|
|
19
|
+
// ✓ auth (route is behind requireAdmin)
|
|
20
|
+
// ⚠ rate-limit (no rateLimit() middleware found)
|
|
21
|
+
// ✓ input validation (express-validator chain present)
|
|
22
|
+
// ✓ audit log (logger.security() called on success path)
|
|
23
|
+
//
|
|
24
|
+
// The rate-limit gap matters because /admin endpoints are common
|
|
25
|
+
// credential-stuffing targets. A 5-line fix:
|
|
26
|
+
//
|
|
27
|
+
// ```js
|
|
28
|
+
// import rateLimit from 'express-rate-limit';
|
|
29
|
+
// app.use('/api/admin', rateLimit({ windowMs: 15*60_000, max: 30 }));
|
|
30
|
+
// ```"
|
|
31
|
+
//
|
|
32
|
+
// The pure-narrative format optimizes for SCREENSHOTABILITY — engineers
|
|
33
|
+
// share security-tool comments that read like a person, not a table.
|
|
34
|
+
//
|
|
35
|
+
// Generation is deterministic (no LLM) for the v1: we render from the
|
|
36
|
+
// delta + a per-CWE narrative template. A future v2 could optionally
|
|
37
|
+
// route through an LLM for richer prose when AGENTIC_SECURITY_LLM_ENDPOINT
|
|
38
|
+
// is configured.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
const SEVERITY_GLYPH = {
|
|
43
|
+
critical: '🟥',
|
|
44
|
+
high: '🟧',
|
|
45
|
+
medium: '🟨',
|
|
46
|
+
low: '🟦',
|
|
47
|
+
info: '⬜',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const CWE_NARRATIVE = {
|
|
51
|
+
'CWE-89': { name: 'SQL injection', why: 'A malicious payload like `1\' OR 1=1--` would dump every row.' },
|
|
52
|
+
'CWE-79': { name: 'XSS', why: 'An attacker who controls this string can execute JavaScript in another user\'s browser.' },
|
|
53
|
+
'CWE-78': { name: 'Command injection', why: 'A `;rm -rf /` style payload would run with the privileges of your service account.' },
|
|
54
|
+
'CWE-22': { name: 'path traversal', why: 'A `../../etc/passwd` style payload would read files outside the intended directory.' },
|
|
55
|
+
'CWE-918': { name: 'SSRF', why: 'An attacker can pivot to the cloud metadata endpoint (`169.254.169.254`) or internal services.' },
|
|
56
|
+
'CWE-502': { name: 'insecure deserialization', why: 'A crafted payload triggers gadget chains — typically remote code execution.' },
|
|
57
|
+
'CWE-611': { name: 'XXE', why: 'A malicious DOCTYPE can exfiltrate local files or trigger SSRF via entity expansion.' },
|
|
58
|
+
'CWE-94': { name: 'template injection', why: 'A `{{7*7}}` style payload escapes to the template engine\'s expression evaluator (often RCE).' },
|
|
59
|
+
'CWE-1321': { name: 'prototype pollution', why: 'A `__proto__` injection can alter the behavior of every downstream object check.' },
|
|
60
|
+
'CWE-352': { name: 'CSRF', why: 'A cross-origin form can trigger authenticated state changes without the user\'s knowledge.' },
|
|
61
|
+
'CWE-601': { name: 'open redirect', why: 'Trusted-domain bouncer for phishing — credentials get harvested on the second hop.' },
|
|
62
|
+
'CWE-113': { name: 'HTTP response splitting', why: 'CR/LF injection lets an attacker forge an entire response, including cache poisoning.' },
|
|
63
|
+
'CWE-798': { name: 'hardcoded secret', why: 'Committed credentials are scraped by automated scanners minutes after a push.' },
|
|
64
|
+
'CWE-327': { name: 'weak crypto', why: 'Modern adversaries can crack MD5/SHA-1/RC4/3DES at practical cost.' },
|
|
65
|
+
'CWE-1333': { name: 'regex DoS', why: 'A pathological input freezes the worker for seconds — DoS without much effort.' },
|
|
66
|
+
'CWE-90': { name: 'LDAP injection', why: 'An attacker can extend the filter to enumerate users or bypass auth.' },
|
|
67
|
+
'CWE-643': { name: 'XPath injection', why: 'A `\' or \'1\'=\'1` payload can return data the query never intended to expose.' },
|
|
68
|
+
'CWE-1336': { name: 'prompt injection', why: 'Untrusted text in the LLM context can override your system prompt.' },
|
|
69
|
+
'CWE-269': { name: 'privilege escalation', why: 'A low-privilege actor can reach a higher-privilege capability through this surface.' },
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function _topCwes(findings, n = 3) {
|
|
73
|
+
const counts = new Map();
|
|
74
|
+
for (const f of findings) {
|
|
75
|
+
const k = f.cwe || 'unknown';
|
|
76
|
+
counts.set(k, (counts.get(k) || 0) + 1);
|
|
77
|
+
}
|
|
78
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, n);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function _route(f) {
|
|
82
|
+
// Best-effort: pull a route-ish string from the file path / vuln text.
|
|
83
|
+
// Many SAST findings carry the route via `f.route` or in the vuln name.
|
|
84
|
+
if (f.route) return f.route;
|
|
85
|
+
const m = (f.vuln || '').match(/\b(GET|POST|PUT|DELETE|PATCH)\s+([\/\w:-]+)/i);
|
|
86
|
+
if (m) return `${m[1].toUpperCase()} ${m[2]}`;
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Top-level renderer. Takes a delta produced by `computePrDelta` and
|
|
92
|
+
* emits a Markdown string suitable for posting as a single PR comment.
|
|
93
|
+
*
|
|
94
|
+
* Mode is chosen automatically:
|
|
95
|
+
* - "clean": no introduced + (resolved > 0 || persistent > 0)
|
|
96
|
+
* - "trivial": no introduced + nothing resolved
|
|
97
|
+
* - "needs-work": one or more introduced findings
|
|
98
|
+
*/
|
|
99
|
+
function renderPrComment(delta, { repoName, prNumber, prTitle } = {}) {
|
|
100
|
+
if (!delta) return '_no security delta available_';
|
|
101
|
+
const intro = delta.introduced || [];
|
|
102
|
+
const resolved = delta.resolved || [];
|
|
103
|
+
const shifted = delta.shifted || [];
|
|
104
|
+
const heading = repoName && prNumber
|
|
105
|
+
? `### 🛡 agentic-security on ${repoName}#${prNumber}`
|
|
106
|
+
: `### 🛡 agentic-security`;
|
|
107
|
+
if (intro.length === 0 && resolved.length === 0 && shifted.length === 0) {
|
|
108
|
+
return [
|
|
109
|
+
heading,
|
|
110
|
+
``,
|
|
111
|
+
`No security delta from \`${delta.baseRef}\` → \`${delta.headRef}\`. ` +
|
|
112
|
+
`${delta.changedFiles.length} file${delta.changedFiles.length === 1 ? '' : 's'} touched; ` +
|
|
113
|
+
`nothing in those changes introduced or resolved a finding. **Safe to merge.**`,
|
|
114
|
+
``,
|
|
115
|
+
`<sub>Scanned ${delta.head?.summary?.total ?? 0} pre-existing findings on the head ref ` +
|
|
116
|
+
`(${delta.base?.summary?.total ?? 0} on base) — none of them moved in this PR.</sub>`,
|
|
117
|
+
].join('\n');
|
|
118
|
+
}
|
|
119
|
+
if (intro.length === 0 && (resolved.length || shifted.length)) {
|
|
120
|
+
const r = delta.summary?.resolved || {};
|
|
121
|
+
return [
|
|
122
|
+
heading,
|
|
123
|
+
``,
|
|
124
|
+
`This PR **resolves** ${resolved.length} finding${resolved.length === 1 ? '' : 's'}` +
|
|
125
|
+
(r.critical || r.high ? ` (including ${r.critical} critical + ${r.high} high)` : '') +
|
|
126
|
+
` and introduces **none**. Nice cleanup work — safe to merge. ✨`,
|
|
127
|
+
``,
|
|
128
|
+
`<sub>${delta.changedFiles.length} file${delta.changedFiles.length === 1 ? '' : 's'} ` +
|
|
129
|
+
`touched between \`${delta.baseRef}\` and \`${delta.headRef}\`.</sub>`,
|
|
130
|
+
].join('\n');
|
|
131
|
+
}
|
|
132
|
+
// needs-work mode: narrative + per-finding paragraphs.
|
|
133
|
+
const lines = [];
|
|
134
|
+
lines.push(heading);
|
|
135
|
+
lines.push('');
|
|
136
|
+
const topCwes = _topCwes(intro);
|
|
137
|
+
const cweSummary = topCwes.map(([cwe, n]) => {
|
|
138
|
+
const meta = CWE_NARRATIVE[cwe];
|
|
139
|
+
return meta ? `${n} ${meta.name}` : `${n} ${cwe}`;
|
|
140
|
+
}).join(', ');
|
|
141
|
+
const hint = prTitle ? ` in "${prTitle}"` : '';
|
|
142
|
+
lines.push(`I looked at the ${delta.changedFiles.length} file${delta.changedFiles.length === 1 ? '' : 's'} ` +
|
|
143
|
+
`you changed${hint} and noticed **${intro.length} new finding${intro.length === 1 ? '' : 's'}** ` +
|
|
144
|
+
`that wasn't on \`${delta.baseRef}\`. Top concerns: ${cweSummary}.`);
|
|
145
|
+
lines.push('');
|
|
146
|
+
// Per-introduced-finding paragraph (cap at 5 for readability).
|
|
147
|
+
const SHOW = intro.slice(0, 5);
|
|
148
|
+
for (const f of SHOW) {
|
|
149
|
+
const meta = CWE_NARRATIVE[f.cwe];
|
|
150
|
+
const sev = SEVERITY_GLYPH[f.severity] || '⬜';
|
|
151
|
+
const route = _route(f);
|
|
152
|
+
const where = route ? `\`${route}\` (\`${f.file}:${f.line}\`)` : `\`${f.file}:${f.line}\``;
|
|
153
|
+
lines.push(`${sev} **${meta?.name || (0,_util_untrusted_js__WEBPACK_IMPORTED_MODULE_0__/* .escapeMarkdown */ .FV)(f.vuln)}** — ${where}`);
|
|
154
|
+
if (meta) lines.push(` > ${meta.why}`);
|
|
155
|
+
if (f.remediation) {
|
|
156
|
+
const onelineFix = String(f.remediation).split('\n')[0].slice(0, 240);
|
|
157
|
+
lines.push(` Suggested fix: ${onelineFix}`);
|
|
158
|
+
}
|
|
159
|
+
if (f.confidence != null && f.confidence < 0.7) {
|
|
160
|
+
lines.push(` <sub>Lower confidence (${(f.confidence * 100).toFixed(0)}%) — may be a false positive worth a quick look.</sub>`);
|
|
161
|
+
}
|
|
162
|
+
lines.push('');
|
|
163
|
+
}
|
|
164
|
+
if (intro.length > SHOW.length) {
|
|
165
|
+
lines.push(`<sub>+${intro.length - SHOW.length} more new finding${intro.length - SHOW.length === 1 ? '' : 's'} — see the scan output for the full list.</sub>`);
|
|
166
|
+
lines.push('');
|
|
167
|
+
}
|
|
168
|
+
if (resolved.length) {
|
|
169
|
+
lines.push(`On the bright side: this PR **resolved ${resolved.length} pre-existing finding${resolved.length === 1 ? '' : 's'}**. 👏`);
|
|
170
|
+
lines.push('');
|
|
171
|
+
}
|
|
172
|
+
const i = delta.summary?.introduced || {};
|
|
173
|
+
const blockMerge = (i.critical || 0) + (i.high || 0) > 0;
|
|
174
|
+
if (blockMerge) {
|
|
175
|
+
lines.push(`---`);
|
|
176
|
+
lines.push(`**Blocking merge:** ${i.critical || 0} critical + ${i.high || 0} high severity ` +
|
|
177
|
+
`finding${(i.critical || 0) + (i.high || 0) === 1 ? '' : 's'} introduced. ` +
|
|
178
|
+
`Fix or suppress with \`// agentic-security-ignore: <rule-id>\` before merging.`);
|
|
179
|
+
} else {
|
|
180
|
+
lines.push(`---`);
|
|
181
|
+
lines.push(`Non-blocking: no critical/high severity findings introduced — but consider addressing the items above before this becomes harder to revisit.`);
|
|
182
|
+
}
|
|
183
|
+
return lines.join('\n');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const _internal = { CWE_NARRATIVE, SEVERITY_GLYPH, _topCwes };
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
/***/ })
|
|
190
|
+
|
|
191
|
+
};
|