@clear-capabilities/agentic-security-scanner 0.183.1 → 0.185.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 +88 -0
- package/dist/{2a8456e0800aeee871b6.json → 0b301d45b231f144a1b4.json} +27 -9
- package/dist/1128.index.js +908 -0
- package/dist/1336.index.js +261 -112
- package/dist/1665.index.js +828 -0
- package/dist/2923.index.js +17 -9
- package/dist/9382.index.js +23 -2
- package/dist/9579.index.js +11 -4
- package/dist/agentic-security.mjs +15 -15
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/frontend/src/shell.js +12 -2
- package/package.json +27 -9
- package/src/context-intelligence/controls.js +15 -0
- package/src/context-intelligence/reachability.js +8 -2
- package/src/dataflow/CLAUDE.md +43 -1
- package/src/dataflow/backward.js +42 -7
- package/src/dataflow/cross-service-taint.js +83 -1
- package/src/dataflow/demand-query.js +929 -0
- package/src/dataflow/dep-fingerprint.js +718 -0
- package/src/dataflow/engine.js +48 -6
- package/src/dataflow/ifds-precise.js +22 -0
- package/src/dataflow/incremental-replay.js +118 -0
- package/src/dataflow/incremental-session.js +257 -0
- package/src/dataflow/incremental.js +237 -35
- package/src/dataflow/index.js +25 -94
- package/src/dataflow/summaries.js +72 -6
- package/src/dataflow/tabulation.js +80 -24
- package/src/discovery/hunter.js +6 -2
- package/src/discovery/index.js +5 -2
- package/src/engine.js +63 -3
- package/src/graph/CLAUDE.md +31 -0
- package/src/graph/adapters/artifact.js +81 -0
- package/src/graph/adapters/crossrepo.js +75 -0
- package/src/graph/adapters/dataflow.js +76 -0
- package/src/graph/adapters/provenance.js +92 -0
- package/src/graph/adapters/semantic.js +89 -0
- package/src/graph/crosswalk-builders.js +152 -0
- package/src/graph/crosswalk.js +146 -0
- package/src/graph/index.js +15 -0
- package/src/graph/observation.js +57 -0
- package/src/graph/refs.js +105 -0
- package/src/graph/registry.js +129 -0
- package/src/graph/relations.js +81 -0
- package/src/graph/scc.js +241 -0
- package/src/graph/view.js +254 -0
- package/src/language/haskell-disclosure.js +13 -1
- package/src/lineage/CLAUDE.md +71 -8
- package/src/lineage/DESIGN_GRAPH_BUILDER.md +1 -1
- package/src/lineage/DESIGN_PATH_PROVENANCE.md +6 -0
- package/src/lineage/boundary-contract.js +273 -0
- package/src/lineage/completion.js +205 -0
- package/src/lineage/continuation.js +140 -0
- package/src/lineage/control-credit.js +313 -0
- package/src/lineage/correlation.js +199 -0
- package/src/lineage/coverage.js +8 -1
- package/src/lineage/cross-repo-link.js +22 -0
- package/src/lineage/cut-planner.js +548 -0
- package/src/lineage/driver.js +219 -66
- package/src/lineage/engine.js +166 -21
- package/src/lineage/export-json.js +5 -0
- package/src/lineage/graph-builder.js +84 -7
- package/src/lineage/haskell-view.js +59 -0
- package/src/lineage/index.js +3 -0
- package/src/lineage/nix-view.js +35 -0
- package/src/lineage/observation-adapters.js +17 -9
- package/src/lineage/owned-build.js +77 -0
- package/src/lineage/owned-worker-entry.js +22 -0
- package/src/lineage/owned-worker.js +95 -0
- package/src/lineage/path-evidence.js +123 -0
- package/src/lineage/path-predicates.js +529 -0
- package/src/lineage/path-query.js +189 -20
- package/src/lineage/path-store.js +191 -17
- package/src/lineage/scc-solver.js +186 -0
- package/src/lineage/smt-orchestrator.js +138 -0
- package/src/lineage/source-seeding.js +36 -2
- package/src/lineage/summaries.js +66 -4
- package/src/lineage/value-versions.js +130 -0
- package/src/lineage/witness-selection.js +95 -0
- package/src/pipeline/scan-health.js +19 -0
- package/src/posture/CLAUDE.md +13 -0
- package/src/posture/evidence-bundle.js +1 -1
- package/src/posture/witness-bundle.js +573 -0
- package/src/posture/witness-runtime.js +276 -0
- package/src/report/index.js +2 -0
package/dist/2923.index.js
CHANGED
|
@@ -443,9 +443,10 @@ function parseOtelJsonObservations(text, context) {
|
|
|
443
443
|
* `destination.service`), `protocol` (normalized — Envoy writes
|
|
444
444
|
* `"HTTP/1.1"`, which contains `/` and is not an approved identifier
|
|
445
445
|
* shape; lowercased and `/` stripped, e.g. `"http1.1"`), and
|
|
446
|
-
* `response_flags` (`'-'`/empty means no flag, i.e. allowed;
|
|
447
|
-
*
|
|
448
|
-
*
|
|
446
|
+
* `response_flags` (`'-'`/empty means no flag, i.e. allowed; `"UAEX"`, the
|
|
447
|
+
* external authorization service denying the request, is `'denied'` via
|
|
448
|
+
* `network.securityDecision`; any other flag, such as an upstream failure or
|
|
449
|
+
* a missing route, is an operational outcome and carries no decision). `start_time`/`end_time` (or
|
|
449
450
|
* `start_time` alone, with `duration` in milliseconds added) feed
|
|
450
451
|
* `firstObservedAt`/`lastObservedAt`. Everything else — client address,
|
|
451
452
|
* request path/query, user agent, request id, response code's own exact
|
|
@@ -488,7 +489,13 @@ function parseEnvoyAccessLogObservations(text, context) {
|
|
|
488
489
|
if (cluster) attributes['destination.service'] = cluster;
|
|
489
490
|
if (protocol) attributes['network.protocol'] = protocol;
|
|
490
491
|
if (typeof record.response_flags === 'string') {
|
|
491
|
-
|
|
492
|
+
// Only a flag that records an authorization decision is a security decision. UAEX is the external authorization service
|
|
493
|
+
// denying the request. Every other flag (UF upstream connection failure, NR no route, UT timeout, DC client disconnect) is
|
|
494
|
+
// an operational outcome, and calling it "denied" would invent a denial, so those make no claim either way.
|
|
495
|
+
const flags = record.response_flags.trim();
|
|
496
|
+
const tokens = flags.split(',').map((t) => t.trim()).filter(Boolean);
|
|
497
|
+
if (flags.length === 0 || flags === '-') attributes['network.securityDecision'] = 'allowed';
|
|
498
|
+
else if (tokens.includes('UAEX')) attributes['network.securityDecision'] = 'denied';
|
|
492
499
|
}
|
|
493
500
|
|
|
494
501
|
if (Object.keys(attributes).length === 0) continue; // nothing safe/present to report for this line
|
|
@@ -520,10 +527,8 @@ function parseEnvoyAccessLogObservations(text, context) {
|
|
|
520
527
|
* `destination.service` — never `objectRef.name`, which is frequently a
|
|
521
528
|
* user-chosen, potentially sensitive resource name) and
|
|
522
529
|
* `responseStatus.code` (mapped to `network.securityDecision`: `< 400`
|
|
523
|
-
* is `'allowed'`,
|
|
524
|
-
* decision
|
|
525
|
-
* failures surface as 401/403/409, which is exactly the "security
|
|
526
|
-
* decision" fact this adapter exists to carry). `requestReceivedTimestamp`/
|
|
530
|
+
* is `'allowed'`, `401`/`403` is `'denied'`, anything else carries no
|
|
531
|
+
* decision: a not-found or a conflict is not an authorization outcome). `requestReceivedTimestamp`/
|
|
527
532
|
* `stageTimestamp` feed the observed-at window. **`user.username` and
|
|
528
533
|
* every request/response body field are deliberately never read** — a
|
|
529
534
|
* K8s audit event's `user` block is exactly the "authenticated identity"
|
|
@@ -565,7 +570,10 @@ function parseK8sAuditLogObservations(text, context) {
|
|
|
565
570
|
if (namespace && resource) attributes['destination.service'] = `${namespace}:${resource}`;
|
|
566
571
|
else if (resource) attributes['destination.service'] = resource;
|
|
567
572
|
if (typeof code === 'number' && Number.isFinite(code)) {
|
|
568
|
-
|
|
573
|
+
// 401 and 403 are the authentication and authorization outcomes. Any other client or server error (404 not found, 409 conflict,
|
|
574
|
+
// 422 invalid, 5xx) is an operational outcome and makes no claim, the same rule as the proxy adapters.
|
|
575
|
+
if (code < 400) attributes['network.securityDecision'] = 'allowed';
|
|
576
|
+
else if (code === 401 || code === 403) attributes['network.securityDecision'] = 'denied';
|
|
569
577
|
}
|
|
570
578
|
|
|
571
579
|
if (Object.keys(attributes).length === 0) continue;
|
package/dist/9382.index.js
CHANGED
|
@@ -1442,6 +1442,9 @@ const AUTH_NAME = /(?:^|[.:])(?:(?:require|ensure|check|assert|verify|validate|e
|
|
|
1442
1442
|
|
|
1443
1443
|
const SENSITIVE_NAME = /(?:^|[.:])(?:query|queryOne|queryRow|exec|execute|executeQuery|raw|rawQuery|prepare|find|findAll|findOne|findById|findByPk|findUnique|findFirst|findMany|get|getById|getBy|select|selectList|selectFirst|insert|insertOne|insertMany|insert_|update|updateOne|updateMany|upsert|delete|deleteOne|deleteMany|remove|destroy|save|create|createMany|aggregate|runDB|writeFile|writeFileSync|appendFile|unlink|rm|rmdir|spawn|spawnSync|execSync|execFile|system|popen|fetch|request|post|put|patch)$/i;
|
|
1444
1444
|
const DB_RECEIVER = /(?:^|\b)(?:db|database|conn|connection|pool|client|cursor|session|repo|repository|dao|model|models|collection|table|prisma|knex|sequelize|orm|store|objects|manager|em|entityManager|mongo|redis|cache|conn|stmt|statement|fs|child_process|cp|axios|http|https|requests|httpx|got|superagent|Database|Persist|Yesod|System)\b/;
|
|
1445
|
+
// Flask-SQLAlchemy `Model.query...`, SQLAlchemy `session.query/add/delete/merge/execute/get`, and the query-builder methods that end a chain.
|
|
1446
|
+
const PY_ORM = /(?:\b[A-Z]\w*\.query\b|\b(?:db\.)?session\.(?:query|add|add_all|delete|merge|execute|get|bulk_save_objects)\b|\.(?:filter_by|get_or_404|first_or_404|one_or_404)\()/;
|
|
1447
|
+
const PY_ORM_WRITE = /\.(?:delete|update|bulk_update_mappings|bulk_insert_mappings)\(|\b(?:db\.)?session\.(?:add|add_all|delete|merge|bulk_save_objects)\b/;
|
|
1445
1448
|
const SQL_TEXT = /\b(?:select\s[\s\S]{1,120}\sfrom|insert\s+into|update\s+\w+\s+set|delete\s+from)\b/i;
|
|
1446
1449
|
const HS_SENSITIVE = /(?:^|\.)(?:runDB|selectList|selectFirst|getBy|getJust|insert_?|insertEntity|update|updateWhere|delete|deleteWhere|replace|upsert|query|query_|execute|execute_|readFile|writeFile|callProcess|readProcess)$|^(?:Database\.|System\.Process\.|System\.Directory\.)/;
|
|
1447
1450
|
|
|
@@ -1601,6 +1604,18 @@ function analyzeSymbolText(symbol, lines, { haskell = false } = {}) {
|
|
|
1601
1604
|
}
|
|
1602
1605
|
}
|
|
1603
1606
|
}
|
|
1607
|
+
// Python ORM calls. `Model.query.filter_by(...).delete()` and `db.session.add(x)` name no database receiver the call-name rule
|
|
1608
|
+
// recognises, and the chained `.delete()` is not a call on one, so the data operation was invisible and a route that
|
|
1609
|
+
// deletes a row read as touching nothing sensitive. A write is a chained mutation or a session write; anything else is a read.
|
|
1610
|
+
if (!haskell && PY_ORM.test(raw)) {
|
|
1611
|
+
const orm = PY_ORM_WRITE.test(raw) ? 'write' : 'read';
|
|
1612
|
+
push({ line: ln, category: 'sensitive', kind: `data-${orm}`, subkind: orm, evidence: 'observed', basis: 'orm-call', rejectionSeen: null, text: clip(raw) });
|
|
1613
|
+
const stmt = statementAt(lines, ln);
|
|
1614
|
+
const pr = ownerBinding(stmt, extraPrincipal);
|
|
1615
|
+
if (pr) {
|
|
1616
|
+
push({ line: ln, category: 'control', kind: pr.tenant ? 'tenant-scope' : 'ownership', subkind: null, evidence: pr.level === 'qualified' ? 'observed' : 'inferred', basis: pr.level === 'qualified' ? 'query-filter' : 'query-filter-unqualified', rejectionSeen: null, text: clip(raw) });
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1604
1619
|
if (SQL_TEXT.test(raw)) {
|
|
1605
1620
|
const verb = /\b(?:insert|update|delete)\b/i.test(raw) ? 'write' : 'read';
|
|
1606
1621
|
push({ line: ln, category: 'sensitive', kind: `data-${verb}`, subkind: verb, evidence: 'observed', basis: 'sql-text', rejectionSeen: null, text: clip(raw) });
|
|
@@ -3474,8 +3489,14 @@ function evaluateProtection({ facts, unknowns, omittedWithControls = false, capp
|
|
|
3474
3489
|
const ctl = facts.filter(f => f.category === 'control' && kinds.includes(f.kind));
|
|
3475
3490
|
// A control whose answer is visibly thrown away (a predicate called as a bare statement) guards nothing.
|
|
3476
3491
|
const effective = (f) => f.rejectionSeen !== false;
|
|
3477
|
-
|
|
3478
|
-
|
|
3492
|
+
// Ownership is the one dimension where reading before checking is the ordinary safe shape: load the record by id, refuse unless the
|
|
3493
|
+
// principal owns it, and only then act on it. What an unchecked read can do is bounded by the check that follows before anything is
|
|
3494
|
+
// written, so for ownership and tenant scope the line that must be preceded is the first WRITE. With no write in view the first
|
|
3495
|
+
// sensitive operation of any kind still applies, so a read returned without a check is not excused.
|
|
3496
|
+
const writes = sens.filter(f => f.subkind === 'write' || f.kind === 'filesystem' || f.kind === 'process');
|
|
3497
|
+
const gate = dim === 'ownership' && writes.length ? Math.min(...writes.map(f => f.effLine)) : firstSens;
|
|
3498
|
+
const before = ctl.filter(f => effective(f) && (gate === null || f.effLine < gate || (f.basis === 'query-filter' && f.effLine <= gate)));
|
|
3499
|
+
const after = ctl.filter(f => gate !== null && f.effLine > gate);
|
|
3479
3500
|
let state; let why;
|
|
3480
3501
|
if (before.length) { state = 'present'; why = 'a control precedes the first sensitive operation'; }
|
|
3481
3502
|
else if (after.length) { state = 'evaluated_and_ineffective'; why = 'every control of this kind runs after the first sensitive operation'; }
|
package/dist/9579.index.js
CHANGED
|
@@ -271,8 +271,12 @@ async function runHunter(focusArea, lens, ctx = {}, opts = {}) {
|
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
const candidates = candidatesFromParsed(parsed, focusArea, lens);
|
|
274
|
-
|
|
275
|
-
|
|
274
|
+
// A proposed candidate with no file and line has nowhere to point and is dropped; say how many, so a reply full of them
|
|
275
|
+
// does not read as a reply that found nothing.
|
|
276
|
+
const proposed = Array.isArray(parsed?.candidates) ? parsed.candidates.length : 0;
|
|
277
|
+
const discarded = proposed - candidates.length;
|
|
278
|
+
appendEntry(transcript, { phase: 'result', candidateCount: candidates.length, discarded });
|
|
279
|
+
return { ...base, candidates, degraded: false, reason: null, discarded };
|
|
276
280
|
}
|
|
277
281
|
|
|
278
282
|
// EXTERNAL MODULE: ./src/discovery/confirm.js
|
|
@@ -733,7 +737,7 @@ function makeTaintProbe(perFileIR, callGraph) {
|
|
|
733
737
|
|
|
734
738
|
async function runDeepAnalysisSafe(perFileIR, callGraph) {
|
|
735
739
|
try {
|
|
736
|
-
const { runDeepAnalysis } = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__,
|
|
740
|
+
const { runDeepAnalysis } = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 18561));
|
|
737
741
|
return runDeepAnalysis(perFileIR, callGraph, {});
|
|
738
742
|
} catch {
|
|
739
743
|
return null;
|
|
@@ -835,7 +839,9 @@ async function runDiscovery(ctx = {}, opts = {}) {
|
|
|
835
839
|
const budget = makeBudget(opts);
|
|
836
840
|
// PRD C4 — what previous runs already judged. scanRoot absent => no memory,
|
|
837
841
|
// which is the correct default for a library call with nowhere to persist.
|
|
838
|
-
|
|
842
|
+
let memory = opts.scanRoot ? loadMemory(opts.scanRoot) : null;
|
|
843
|
+
// `--forget-refuted`: a model, a ruleset or the code changed, so what an earlier run refuted is worth examining again.
|
|
844
|
+
if (memory && opts.forgetRefuted) memory = forgetRefuted(memory);
|
|
839
845
|
// Every LLM call in this pipeline goes through this one wrapped callback.
|
|
840
846
|
const llmInvoke = budget.wrap(opts.llmInvoke);
|
|
841
847
|
|
|
@@ -870,6 +876,7 @@ async function runDiscovery(ctx = {}, opts = {}) {
|
|
|
870
876
|
const run = await runHunter(area, lens, { fileContents: ctx.fileContents || {}, scanRoot: opts.scanRoot }, { llmInvoke, scanRoot: opts.scanRoot });
|
|
871
877
|
runs.push({ focusAreaId: run.focusAreaId, lens: run.lens, degraded: run.degraded, reason: run.reason, candidateCount: run.candidates.length });
|
|
872
878
|
if (run.degraded && run.reason) reasons.push(`${area.label} × ${lens.key}: ${run.reason}`);
|
|
879
|
+
if (run.discarded > 0) reasons.push(`${area.label} × ${lens.key}: ${run.discarded} proposed candidate(s) had no usable file and line and were discarded`);
|
|
873
880
|
if (run.degraded) areaDegradedCount += 1;
|
|
874
881
|
else hunted.add(area.id);
|
|
875
882
|
candidates = candidates.concat(run.candidates);
|