@clear-capabilities/agentic-security-scanner 0.195.28 → 0.195.30
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 +41 -0
- package/bin/agentic-security.js +13 -4
- package/dist/1340.index.js +11 -6
- package/dist/3500.index.js +943 -0
- package/dist/5616.index.js +146 -19
- package/dist/{3eabd90d6d9c8c8dbbd9.json → 7e58a58b8e6ccdde1659.json} +7 -6
- package/dist/9560.index.js +29 -5
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +7 -6
- package/src/egress/policy.js +79 -1
- package/src/engine.js +1 -1
- package/src/language/CLAUDE.md +1 -1
- package/src/language/haskell-guards.js +26 -5
- package/src/lineage/CLAUDE.md +3 -1
- package/src/lineage/deployment/adapter-cloud.js +5 -2
- package/src/lineage/deployment/attack-paths.js +107 -30
- package/src/lineage/deployment/boundaries-run.js +2 -2
- package/src/lineage/deployment/iam-eval.js +105 -0
- package/src/lineage/deployment/projection.js +10 -4
- package/src/lineage/deployment/proof-binding.js +79 -0
- package/src/llm-validator/bounded-context.js +115 -8
- package/src/llm-validator/index.js +1 -1
- package/src/mcp/CLAUDE.md +6 -0
- package/src/mcp/tools.js +27 -3
- package/src/pipeline/evidence-record.js +1 -1
- package/src/posture/CLAUDE.md +2 -0
- package/src/posture/assurance/candidate-qualification.js +155 -0
- package/src/posture/assurance/release-package-cli.js +44 -2
- package/src/posture/auditor-walkthrough.js +2 -1
- package/src/posture/dnr/detection-ir.js +2 -2
- package/src/posture/dnr/hunt.js +22 -6
- package/src/posture/dnr/log-reader.js +47 -3
- package/src/posture/dnr/matchers.js +43 -3
- package/src/posture/dnr/outputs.js +16 -2
- package/src/posture/dnr/payload-classes.js +12 -2
- package/src/posture/dnr/validate.js +17 -4
- package/src/posture/evaluation/report.js +2 -1
- package/src/posture/evaluation/score.js +24 -7
- package/src/posture/logic-claims.js +15 -14
- package/src/posture/sibling-queue.js +69 -0
- package/src/posture/triage-memory.js +106 -26
- package/src/posture/verification-separation.js +55 -7
- package/src/report/index.js +4 -0
- package/src/runScan.js +5 -0
- package/src/scope/flags.js +1 -0
- package/src/scope/inventory.js +17 -1
- package/src/scope/release-scope.js +80 -0
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,47 @@
|
|
|
9
9
|
> make the history less accurate, not more.
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
## 0.195.30 - Threat model hardening: verdicts bound to what they tested, honest path limits, a fail-closed restricted profile, and candidate qualification
|
|
13
|
+
|
|
14
|
+
**Measured, 0.195.29:** `TIMINGS preflight=6s push=13s pr=4s checks=737s merge=5s verify=10s tag=2s release=254s npm=146s total=1177s release-clock=271s`, shipped from the merge to the published release in 271 s; the npm listing lagged the publish and `ship --resume` confirmed it.
|
|
15
|
+
|
|
16
|
+
The locally verifiable part of the threat model hardening plan. Each of the nine reproduced baseline behaviours (TM-01 to TM-09) has a failing control that now passes. Staging delivery, independent accuracy measurement, attested execution and owner approvals are separate qualifications and are not claimed: the candidate packet records each as blocked. Nothing moved a maturity. Per-requirement state: `docs/implementation-status/threat-model-hardening.md`.
|
|
17
|
+
|
|
18
|
+
- **Changed results, listed first.**
|
|
19
|
+
- A hunt's target counter counts only events with the matched route template and method, so an unrelated route no longer inflates it.
|
|
20
|
+
- An authoritative `authenticated: false` survives a redacted authorization header: a header alone shows a credential was present, not that it was accepted.
|
|
21
|
+
- An IAM deny blocks only the action, resource and condition it matches; a conditional or opaque deny that can affect the action leaves the path `unresolved` instead of blocked.
|
|
22
|
+
- A family or directory triage bucket is advisory: a new high finding keeps its confidence.
|
|
23
|
+
- Corroboration is counted by independent evidence group, not by lens.
|
|
24
|
+
- **Detection (FR-07).** Credential presence and authentication outcome are separate. A new `object-authz` rule detects an authenticated actor receiving another tenant's object. Hunts report window gaps, and a detection signal is never reported as a corroborated effect on its own. CWE-639 moved to the new payload class; the detection IR documents the change.
|
|
25
|
+
- **Scope (FR-01).** Standard JSON carries the `scope` object and a deterministic `inventoryDigest` over every eligibility decision before display caps. Scope preview agrees with it. A release scope reconciliation reports each critical route or unit as covered, excluded, unsupported or unknown.
|
|
26
|
+
- **IAM and paths (FR-03).** Per-action deny evaluation, the `StringEquals`, `StringNotEquals`, `Bool` and `ArnEquals` condition subset, and `truncation.reasons` that tell depth, expansion, result-count and deadline cutoffs apart. A search that hit a bound cannot certify the absence of a path. `boundaries --action` queries one action. The boundary context version is 1.1.0, with optional fields only.
|
|
27
|
+
- **Proof binding (FR-04).** A deployed path is promoted only by an Ed25519-signed proof binding from a trusted issuer. The binding covers the revision, artifact, policy, actor, tenant, route, scenario and oracle. A receipt from another revision or context keeps its stable id with a distinct diagnostic.
|
|
28
|
+
- **Learned decisions (FR-05).** Exact, scoped, HMAC-signed triage decisions apply; bucket matches, expired, copied, cross-tenant and unsigned records do not. A confirmed root cause yields a sibling review queue with six separate states. The legacy `mitigated` count keeps its detector-coverage meaning.
|
|
29
|
+
- **Restricted egress (FR-06).** `AGENTIC_SECURITY_EGRESS_PROFILE=restricted` requires its authoritative policy and fails closed on a malformed, unreadable, missing or unsupported one before any model request. An operator deny always wins. High-impact MCP approvals can be bound to tool, action, target and change, with an expiry; the restricted profile requires that binding.
|
|
30
|
+
- **Pilot (FR-02, FR-08).** Loopback fixtures cover:
|
|
31
|
+
- a two-tenant records service (PILOT-01 to PILOT-08);
|
|
32
|
+
- the order approval invariant (wrong guard, then patched);
|
|
33
|
+
- an app-to-cloud chain whose object store adjudicates with its own evaluator;
|
|
34
|
+
- a bounded SQLite injection proof (PILOT-12).
|
|
35
|
+
- **Candidate qualification (FR-10).**
|
|
36
|
+
- A signed candidate packet; gates G-01 to G-07 are recomputed from case evidence, never read from the packet.
|
|
37
|
+
- `agentic-security release-package qualify <packet> --public-key <pem>` exits 0 only when qualified.
|
|
38
|
+
- Each case carries a `local`, `staging` or `independent` tier, and a gate that requires a tier (`--require-tier G-05=staging`) counts only cases of that tier.
|
|
39
|
+
- `revalidateDecision` refuses a decision for another artifact, policy, scanner bundle or packet, or after its evidence expires.
|
|
40
|
+
- `npm run pilot:qualify` writes one signed packet per deployment identity. On a developer host it reports "not qualified", with the blocked staging, independent and attested-runner cases named.
|
|
41
|
+
- **Evaluation scoring (FR-09.2).** Every enrolled label is reconciled to one status. A negative label whose target the run never reached used to leave the count without a trace; it is now counted as `negativesOutsideRun`.
|
|
42
|
+
|
|
43
|
+
## 0.195.29 - `scan --watch` watches, the validator collects callers and configuration, and pilot tooling for the measured targets
|
|
44
|
+
|
|
45
|
+
**Measured, 0.195.28:** `TIMINGS preflight=3s push=6s pr=1s checks=430s merge=6s verify=6s tag=2s release=131s npm=450s total=1035s release-clock=145s`, shipped from the merge to the published release in 145 s; the run stopped once on a git index lock held by the post-commit gate warm-up, and `ship --resume` continued after it.
|
|
46
|
+
|
|
47
|
+
- **`scan --watch` exited after its first scan** and never watched anything: the watcher does not block, and the scan command's dispatch exits with whatever the command returns. It now stays running until Ctrl-C or SIGTERM, and says when the first scan is done. A new end-to-end test runs the real CLI with a scope flag: a change to a Git-ignored file does not rescan, and a change to a tracked file under `dist/` (which the default watcher filter skips) does.
|
|
48
|
+
- **The model validator collects callers and configuration** (PRD FR 05). Callers: the nearest function definition above the finding, then up to three call sites of that name in scanned files of the same language, labelled as matched by name rather than resolved. Configuration: environment keys read near the finding, then the scanned configuration files that set them; a real `.env` file is never read, and the value of a secret-like key is masked before redaction. What could not be collected is still stated in the prompt. Prompt version `v2.2-callers-config`, so cached verdicts are not reused.
|
|
49
|
+
- **A Haskell request whose whole destination is a literal is refuted** when the client's text reaches only the query, headers, body, method or authentication: a `constant-input` refutation tied to the file's content hash, kept in the refuted record. A client-chosen path, a concatenation, a custom manager or an unknown record field still only lowers the finding to `info`.
|
|
50
|
+
- **Pilot and benchmark tooling** for the targets that need data the project does not hold. `bench/haskell-pilot/pilot.mjs capture` records what M0 asks for and measures the 95% parse and 90% semantic rates with failures in the denominator, with frozen exclusions; `compare` diffs two reports of one snapshot and says it is not recall. `bench/haskell-core-families/adjudicate.mjs` imports blind two-reviewer labels, reports agreement and Cohen's kappa, adds only unanimous cases, and freezes the manifest and definitions; the runner refuses the holdout without a matching freeze and logs every holdout run, and reports 95% Wilson intervals. No independent case and no pilot snapshot exists yet; every family still reports `insufficient-cases`.
|
|
51
|
+
- **CI**: the cabal store's cache key names the runner's OS release, so packages built on one Ubuntu release are not restored on another.
|
|
52
|
+
|
|
12
53
|
## 0.195.28 - Git-aware scope, Haskell coverage accounting, and findings that keep their evidence
|
|
13
54
|
|
|
14
55
|
**Measured, 0.195.27:** `TIMINGS preflight=4s push=8s pr=4s checks=15s merge=6s verify=6s tag=2s release=131s npm=268s total=444s release-clock=145s`, shipped from the merge to the published release in 145 s.
|
package/bin/agentic-security.js
CHANGED
|
@@ -147,6 +147,7 @@ Commands:
|
|
|
147
147
|
release-view <package.json> Developer, CISO and auditor views of one signed release package (--view, --format text|json|html, --previous, --public-key)
|
|
148
148
|
metrics [path] Assurance Coverage, Evidence Conversion and Verified Remediation Rate for the last scan, each with numerator and denominator (--format text|json)
|
|
149
149
|
release-package build|verify Build a signed release package from the last scan (--private-key, --output, --fix-records), or check one offline (--public-key)
|
|
150
|
+
release-package qualify <packet.json> --public-key <pem>: recompute candidate gates G-01..G-07 from a signed candidate packet (exit 0 only when qualified; --require-case, --require-gate, --require-tier G-05=staging, --output, --format json)
|
|
150
151
|
reset [--yes] [--keep ...] Right-to-delete: wipe accumulated learned state under .agentic-security/ (preserves operator-authored config)
|
|
151
152
|
--expired only remove artifacts past their retention-class TTL
|
|
152
153
|
Every run writes a deletion-report.json proving what was planned/deleted/preserved/failed.
|
|
@@ -181,7 +182,7 @@ Commands:
|
|
|
181
182
|
scan-baseline --current <f> --previous <f>
|
|
182
183
|
Finding-level diff between two scan JSON outputs.
|
|
183
184
|
Reports added / removed / changed findings.
|
|
184
|
-
boundaries --from <dir> [--findings <last-scan.json>] [--environment <name>] [--out <file>] [--json]
|
|
185
|
+
boundaries --from <dir> [--findings <last-scan.json>] [--environment <name>] [--out <file>] [--json] [--action <iam-action>]
|
|
185
186
|
Build a deployment boundary graph from local configuration
|
|
186
187
|
(and optional sanitized traces) and attach a boundary context
|
|
187
188
|
to each finding of a scan result. Read-only, local files only.
|
|
@@ -748,6 +749,7 @@ async function cmdScan(args) {
|
|
|
748
749
|
const seed = await runScan(targetAbs, { onProgress: scanProgressReporter(), ..._watchScanOpts });
|
|
749
750
|
clearScanProgressLine();
|
|
750
751
|
let prevFindings = seed.scan.findings || [];
|
|
752
|
+
process.stderr.write(`[watch] first scan done (${prevFindings.length} finding(s)); watching for changes${_watchScoped ? ' in the scan scope' : ''}\n`);
|
|
751
753
|
await watchProject(targetAbs, async () => {
|
|
752
754
|
try {
|
|
753
755
|
const { scan } = await runScan(targetAbs, { onProgress: scanProgressReporter(), ..._watchScanOpts });
|
|
@@ -761,7 +763,12 @@ async function cmdScan(args) {
|
|
|
761
763
|
process.stderr.write(`[watch] rescan failed: ${e.message}\n`);
|
|
762
764
|
}
|
|
763
765
|
}, _watchScoped ? { isEligible: createScopeFilter(targetAbs, scopeFlags.active ? scopeFlags.scope : {}) } : {});
|
|
764
|
-
|
|
766
|
+
// watchProject does NOT block: it starts its loop and returns. The dispatch is `process.exit(await cmdScan(args))`, so returning here
|
|
767
|
+
// exited right after the first scan and `scan --watch` never watched anything. Stay pending until Ctrl-C or SIGTERM.
|
|
768
|
+
return await new Promise((resolve) => {
|
|
769
|
+
process.once('SIGINT', () => resolve(130));
|
|
770
|
+
process.once('SIGTERM', () => resolve(143));
|
|
771
|
+
});
|
|
765
772
|
}
|
|
766
773
|
|
|
767
774
|
// --incremental : reuse taint summaries from prior scans for faster deep mode.
|
|
@@ -4160,9 +4167,9 @@ Do not suppress anything you are not certain is a false positive. When in doubt,
|
|
|
4160
4167
|
// 3 the feature is disabled, blocked or unsupported on this platform (nothing was read).
|
|
4161
4168
|
async function cmdBoundaries(args) {
|
|
4162
4169
|
const f = args.flags;
|
|
4163
|
-
const usage = 'Usage: agentic-security boundaries --from <dir> [--findings <last-scan.json>] [--environment <name>] [--repository <name>] [--revision <sha>] [--root <project>] [--out <file>] [--json] [--now <iso>]';
|
|
4170
|
+
const usage = 'Usage: agentic-security boundaries --from <dir> [--findings <last-scan.json>] [--environment <name>] [--repository <name>] [--revision <sha>] [--root <project>] [--out <file>] [--json] [--now <iso>] [--action <iam-action>]';
|
|
4164
4171
|
if (typeof f.from !== 'string' || !f.from) { console.error(usage); return 2; }
|
|
4165
|
-
for (const k of ['findings', 'environment', 'repository', 'revision', 'out', 'now', 'root']) {
|
|
4172
|
+
for (const k of ['findings', 'environment', 'repository', 'revision', 'out', 'now', 'root', 'action']) {
|
|
4166
4173
|
if (f[k] !== undefined && (typeof f[k] !== 'string' || !f[k])) { console.error(`agentic-security boundaries: --${k} needs a value\n${usage}`); return 2; }
|
|
4167
4174
|
}
|
|
4168
4175
|
const scanRoot = path.resolve(f.root || '.');
|
|
@@ -4172,6 +4179,8 @@ async function cmdBoundaries(args) {
|
|
|
4172
4179
|
const result = runBoundaries({
|
|
4173
4180
|
config, from: path.resolve(f.from), environment: f.environment || 'prod', repository: f.repository || null, revision: f.revision || null,
|
|
4174
4181
|
findingsFile: f.findings ? path.resolve(f.findings) : null, verifySignature: _verifyLastScan, now: f.now,
|
|
4182
|
+
// threat-model FR-03.2: an action-specific query (`--action s3:GetObject`) decides each IAM hop for that action only
|
|
4183
|
+
action: f.action || null,
|
|
4175
4184
|
});
|
|
4176
4185
|
if (result.status === 'disabled' || result.status === 'blocked' || result.status === 'unsupported') {
|
|
4177
4186
|
console.error(`agentic-security boundaries: not run (${result.status}): ${result.reason}`);
|
package/dist/1340.index.js
CHANGED
|
@@ -666,6 +666,8 @@ function parseCompose(text, fileCtx) {
|
|
|
666
666
|
return kit.result();
|
|
667
667
|
}
|
|
668
668
|
|
|
669
|
+
// EXTERNAL MODULE: ./src/lineage/deployment/iam-eval.js
|
|
670
|
+
var iam_eval = __webpack_require__(1086);
|
|
669
671
|
;// CONCATENATED MODULE: ./src/lineage/deployment/adapter-cloud.js
|
|
670
672
|
// adapter-cloud.js: Terraform plan JSON and IAM policy documents into
|
|
671
673
|
// deployment boundary nodes and edges (X-302).
|
|
@@ -679,6 +681,7 @@ function parseCompose(text, fileCtx) {
|
|
|
679
681
|
|
|
680
682
|
|
|
681
683
|
|
|
684
|
+
|
|
682
685
|
const TERRAFORM_PARSER = 'terraform-plan-json';
|
|
683
686
|
const TERRAFORM_PARSER_VERSION = '1';
|
|
684
687
|
const IAM_PARSER = 'iam-policy-json';
|
|
@@ -708,10 +711,12 @@ function applyPolicy(kit, identity, policy, subject) {
|
|
|
708
711
|
const conditioned = isObject(st.Condition) && Object.keys(st.Condition).length > 0;
|
|
709
712
|
read += 1;
|
|
710
713
|
if (resources.length > MAX_RESOURCES_PER_STATEMENT) kit.gap('limit-exceeded', subject, `a statement lists ${resources.length} resources; only the first ${MAX_RESOURCES_PER_STATEMENT} are read`);
|
|
714
|
+
if (actions.join(',').length > 250) kit.gap('limit-exceeded', subject, 'a statement\'s action list is longer than the graph records; the clipped list leaves decisions about the missing actions unresolved');
|
|
715
|
+
if (conditioned && (0,iam_eval/* statementAttrs */.c2)(actions, st.Condition).conditionSupported !== true) kit.gap('unsupported-syntax', subject, 'a statement condition uses an operator or value outside the supported subset (StringEquals, StringNotEquals, Bool, ArnEquals); decisions it can affect stay unresolved');
|
|
711
716
|
for (const r of resources.slice(0, MAX_RESOURCES_PER_STATEMENT)) {
|
|
712
717
|
const target = kit.node('resource', `aws/${r}`, { trustZone: 'internal', attrs: r === '*' ? { wildcard: true } : {} });
|
|
713
|
-
|
|
714
|
-
|
|
718
|
+
// threat-model FR-03.1: the action list (and whether it was clipped) and the condition, in a form the path search can decide
|
|
719
|
+
const attrs = (0,iam_eval/* statementAttrs */.c2)(actions, conditioned ? st.Condition : null);
|
|
715
720
|
kit.edge('grants', identity, target, {
|
|
716
721
|
effect, confidence: conditioned ? 'low' : 'high', discriminator: `${actions.join(',')}${conditioned ? ';cond' : ''}`.slice(0, 250), attrs,
|
|
717
722
|
});
|
|
@@ -1638,8 +1643,8 @@ function applyCorrelation(graph, correlation) {
|
|
|
1638
1643
|
return (0,boundary_graph/* buildBoundaryGraph */.v4)({ repository: graph.repository, revision: graph.revision, nodes, edges, gaps: graph.gaps, sources: graph.sources });
|
|
1639
1644
|
}
|
|
1640
1645
|
|
|
1641
|
-
// EXTERNAL MODULE: ./src/lineage/deployment/projection.js +
|
|
1642
|
-
var projection = __webpack_require__(
|
|
1646
|
+
// EXTERNAL MODULE: ./src/lineage/deployment/projection.js + 2 modules
|
|
1647
|
+
var projection = __webpack_require__(83929);
|
|
1643
1648
|
;// CONCATENATED MODULE: ./src/lineage/deployment/boundaries-run.js
|
|
1644
1649
|
// boundaries-run.js: the operator entry point that builds a boundary context for scanned findings (X-308 wiring).
|
|
1645
1650
|
//
|
|
@@ -1741,7 +1746,7 @@ function readScanFindings(file, { maxBytes, verify } = {}) {
|
|
|
1741
1746
|
* @param {string} [o.now] ISO time used to judge trace staleness; defaults to the current time
|
|
1742
1747
|
* @returns {object} typed result; `status` is ok | disabled | blocked | unsupported | error. Never throws.
|
|
1743
1748
|
*/
|
|
1744
|
-
function runBoundaries({ config, from, environment = 'prod', repository = null, revision = null, findingsFile = null, findings: suppliedFindings = null, verifySignature, now } = {}) {
|
|
1749
|
+
function runBoundaries({ config, from, environment = 'prod', repository = null, revision = null, findingsFile = null, findings: suppliedFindings = null, verifySignature, now, action = null } = {}) {
|
|
1745
1750
|
const gate = (0,assurance_config/* featureStatus */.FX)(config, DEPLOYMENT_FEATURE);
|
|
1746
1751
|
if (gate.status !== 'ok') return { ...gate, report: null };
|
|
1747
1752
|
const limits = (0,assurance_config/* limitValues */.OH)(config);
|
|
@@ -1818,7 +1823,7 @@ function runBoundaries({ config, from, environment = 'prod', repository = null,
|
|
|
1818
1823
|
if (integrity === 'unsigned') notes.push('the scan result carries no signature, so its integrity was not verified');
|
|
1819
1824
|
}
|
|
1820
1825
|
|
|
1821
|
-
const attached = (0,projection/* attachBoundaryContext */.Nu)({ config, findings, graph, bindings, now: clock });
|
|
1826
|
+
const attached = (0,projection/* attachBoundaryContext */.Nu)({ config, findings, graph, bindings, now: clock, action });
|
|
1822
1827
|
if (attached.status !== 'ok') return { ...attached, report: null };
|
|
1823
1828
|
const contexts = attached.findings.map((f) => f.boundaryContext).filter(Boolean);
|
|
1824
1829
|
const rows = attached.findings.map((f) => ({
|