@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.
- package/dist/178.index.js +1 -1
- package/dist/333.index.js +283 -0
- package/dist/384.index.js +1 -1
- package/dist/637.index.js +1 -1
- package/dist/838.index.js +1 -1
- package/dist/985.index.js +90 -1
- package/dist/agentic-security.mjs +83 -83
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +6 -4
- package/src/.agentic-security/findings.json +104638 -0
- package/src/.agentic-security/last-scan.json +104638 -0
- package/src/.agentic-security/last-scan.json.sig +1 -0
- package/src/.agentic-security/scan-history.json +12562 -0
- package/src/.agentic-security/streak.json +21 -0
- package/src/dataflow/.agentic-security/findings.json +6086 -0
- package/src/dataflow/.agentic-security/last-scan.json +6086 -0
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
- package/src/dataflow/.agentic-security/scan-history.json +250 -0
- package/src/dataflow/.agentic-security/streak.json +21 -0
- package/src/dataflow/cross-service-taint.js +201 -0
- package/src/dataflow/formal-verify.js +204 -0
- package/src/dataflow/ifds-precise.js +222 -0
- package/src/dataflow/k2-summary-cache.js +153 -0
- package/src/dataflow/lib-taint-summaries.js +198 -0
- package/src/dataflow/privacy-taint.js +205 -0
- package/src/dataflow/smt-feasibility.js +189 -0
- package/src/engine.js +784 -127
- package/src/ir/.agentic-security/findings.json +4011 -0
- package/src/ir/.agentic-security/last-scan.json +4011 -0
- package/src/ir/.agentic-security/last-scan.json.sig +1 -0
- package/src/ir/.agentic-security/scan-history.json +193 -0
- package/src/ir/.agentic-security/streak.json +20 -0
- package/src/ir/cpp-preprocessor.js +142 -0
- package/src/ir/csharp-ir.js +604 -0
- package/src/ir/universal-ir.js +403 -0
- package/src/mcp/.agentic-security/findings.json +8632 -0
- package/src/mcp/.agentic-security/last-scan.json +8632 -0
- package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
- package/src/mcp/.agentic-security/scan-history.json +143 -0
- package/src/mcp/.agentic-security/streak.json +20 -0
- package/src/mcp/tools.js +90 -1
- package/src/posture/.agentic-security/findings.json +64004 -0
- package/src/posture/.agentic-security/last-scan.json +64004 -0
- package/src/posture/.agentic-security/last-scan.json.sig +1 -0
- package/src/posture/.agentic-security/scan-history.json +7162 -0
- package/src/posture/.agentic-security/streak.json +21 -0
- package/src/posture/api-contract.js +193 -0
- package/src/posture/attack-taxonomy.js +227 -0
- package/src/posture/compliance-policy.js +218 -0
- package/src/posture/composite-risk.js +122 -0
- package/src/posture/csharp-analysis.js +330 -0
- package/src/posture/exploit-bundle.js +210 -0
- package/src/posture/federated-learning.js +172 -0
- package/src/posture/license-attributions.js +94 -0
- package/src/posture/license-graph.js +238 -0
- package/src/posture/pqc-migration-plan.js +158 -0
- package/src/posture/reachability-filter.js +33 -2
- package/src/posture/realtime-cve-monitor.js +214 -0
- package/src/posture/runtime-correlation.js +174 -0
- package/src/posture/sbom-diff.js +171 -0
- package/src/posture/sca-policy.js +235 -0
- package/src/posture/sca-upgrade.js +259 -0
- package/src/posture/threat-model-auto.js +268 -0
- package/src/posture/triage-learning.js +170 -0
- package/src/posture/triage.js +26 -1
- package/src/sast/.agentic-security/findings.json +6154 -0
- package/src/sast/.agentic-security/last-scan.json +6154 -0
- package/src/sast/.agentic-security/last-scan.json.sig +1 -0
- package/src/sast/.agentic-security/scan-history.json +941 -0
- package/src/sast/.agentic-security/streak.json +22 -0
- package/src/sast/_secret-entropy.js +145 -0
- package/src/sast/cloud-iam.js +312 -0
- package/src/sast/cpp.js +138 -4
- package/src/sast/crypto-protocol.js +388 -0
- package/src/sast/csharp-tokenizer.js +392 -0
- package/src/sast/csharp.js +924 -138
- package/src/sast/dapp-frontend.js +200 -0
- package/src/sast/k8s-admission.js +271 -0
- package/src/sast/llm-app.js +272 -0
- package/src/sast/ml-supply-chain.js +259 -0
- package/src/sast/mobile.js +224 -0
- package/src/sast/post-quantum-crypto.js +348 -0
- package/src/sast/web3-advanced.js +375 -0
- package/src/sca/.agentic-security/findings.json +7460 -0
- package/src/sca/.agentic-security/last-scan.json +7460 -0
- package/src/sca/.agentic-security/last-scan.json.sig +1 -0
- package/src/sca/.agentic-security/scan-history.json +113 -0
- package/src/sca/.agentic-security/streak.json +21 -0
- package/src/sca/CLAUDE.md +161 -0
- package/src/sca/binary-metadata.js +37 -15
- package/src/sca/sigstore-verify.js +215 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"firstScanDate": "2026-05-28T18:57:47.945Z",
|
|
3
|
+
"lastScanDate": "2026-05-29T17:04:08.207Z",
|
|
4
|
+
"totalScans": 45,
|
|
5
|
+
"daysCleanCritical": 2,
|
|
6
|
+
"lastCleanDate": "2026-05-29",
|
|
7
|
+
"lastCriticalDate": null,
|
|
8
|
+
"hasEverHadCritical": false,
|
|
9
|
+
"bestDaysCleanCritical": 2,
|
|
10
|
+
"totalFindingsAtFirstScan": 28,
|
|
11
|
+
"totalFindingsAtLastScan": 32,
|
|
12
|
+
"totalFixesInferred": 1,
|
|
13
|
+
"lastGrade": "A-",
|
|
14
|
+
"bestGrade": "A-",
|
|
15
|
+
"launchCheckPassedAt": null,
|
|
16
|
+
"achievements": [
|
|
17
|
+
"first-fix",
|
|
18
|
+
"first-scan",
|
|
19
|
+
"scan-veteran-25"
|
|
20
|
+
],
|
|
21
|
+
"previousGrade": "A-"
|
|
22
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Shannon entropy + dictionary-word filter for secret-shaped strings.
|
|
2
|
+
//
|
|
3
|
+
// Real high-entropy credentials (API keys, JWTs, base64 tokens) score
|
|
4
|
+
// ≥3.5 bits per character on Shannon's formula because they draw from a
|
|
5
|
+
// large alphabet uniformly. Dictionary words and template values
|
|
6
|
+
// ("password", "changeme", "myTodo") score much lower.
|
|
7
|
+
//
|
|
8
|
+
// Combined with a small common-word block list, this filter drops the
|
|
9
|
+
// "468 FPs / 1 TP" pattern on Juliet Java (test scaffolding uses short
|
|
10
|
+
// dictionary words as fake credentials) without losing recall on real
|
|
11
|
+
// secrets.
|
|
12
|
+
|
|
13
|
+
// Compact common-word block list (top ~120). Anything appearing here is
|
|
14
|
+
// rejected as a credential candidate regardless of length / entropy.
|
|
15
|
+
// Source: union of OWASP common-passwords + frequent-English + idiomatic
|
|
16
|
+
// security-test placeholders.
|
|
17
|
+
const COMMON_WORDS = new Set([
|
|
18
|
+
// Frequent English (often appear as test placeholders)
|
|
19
|
+
'hello', 'world', 'example', 'demo', 'sample', 'foo', 'bar', 'baz', 'qux',
|
|
20
|
+
'todo', 'tbd', 'unknown', 'undefined', 'null', 'none', 'placeholder', 'change',
|
|
21
|
+
'changeme', 'default', 'replace', 'replaceme', 'value', 'string', 'text',
|
|
22
|
+
// Common bad-secret values (frequent in fake credentials)
|
|
23
|
+
'password', 'passwd', 'secret', 'admin', 'root', 'guest', 'test', 'testing',
|
|
24
|
+
'temp', 'tmp', 'temporary', 'production', 'staging', 'development', 'localhost',
|
|
25
|
+
// Juliet test conventions
|
|
26
|
+
'sourcedata', 'data', 'badsource', 'goodsource', 'tainted', 'untrusted',
|
|
27
|
+
'hardcoded', 'source', 'sink', 'bad', 'good', 'value', 'demo',
|
|
28
|
+
// Common short placeholders + frequent literal values
|
|
29
|
+
'enabled', 'disabled', 'yes', 'no', 'true', 'false', 'on', 'off',
|
|
30
|
+
'allow', 'deny', 'permit', 'always', 'never', 'auto', 'manual',
|
|
31
|
+
'public', 'private', 'protected', 'internal', 'external',
|
|
32
|
+
'localhost', '127.0.0.1', '0.0.0.0', 'example.com', 'mysite.com',
|
|
33
|
+
// Test-fixture credential values (frequent in pen-test / fixture data)
|
|
34
|
+
'hunter2', 'iloveyou', 'qwerty', 'monkey', 'letmein', 'dragon', 'master',
|
|
35
|
+
'football', 'baseball', 'sunshine', 'princess', 'welcome',
|
|
36
|
+
// Email-shaped placeholders
|
|
37
|
+
'user@example.com', 'admin@example.com', 'test@example.com', 'noreply',
|
|
38
|
+
// Common config / framework strings
|
|
39
|
+
'utf-8', 'utf8', 'iso-8859-1', 'ascii', 'application', 'json', 'xml',
|
|
40
|
+
'http', 'https', 'ftp', 'tcp', 'udp', 'smtp', 'pop', 'imap',
|
|
41
|
+
'localhost:3000', 'localhost:8080', 'localhost:8000', 'localhost:5000',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
// Shannon entropy in bits per character.
|
|
45
|
+
export function shannonEntropy(s) {
|
|
46
|
+
if (!s || s.length === 0) return 0;
|
|
47
|
+
const freq = new Map();
|
|
48
|
+
for (const c of s) freq.set(c, (freq.get(c) || 0) + 1);
|
|
49
|
+
let h = 0;
|
|
50
|
+
for (const count of freq.values()) {
|
|
51
|
+
const p = count / s.length;
|
|
52
|
+
h -= p * Math.log2(p);
|
|
53
|
+
}
|
|
54
|
+
return h;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Quick base64 / hex / JWT detection — these are NEVER dictionary words.
|
|
58
|
+
// We short-circuit the heavier checks for known credential shapes.
|
|
59
|
+
const BASE64ISH_RE = /^[A-Za-z0-9+/=_-]{16,}$/;
|
|
60
|
+
const HEX_RE = /^[0-9a-fA-F]{16,}$/;
|
|
61
|
+
const JWT_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
62
|
+
|
|
63
|
+
// Configurable thresholds. Defaults tuned against the Juliet Java
|
|
64
|
+
// 468-FP / 1-TP collapse — these settings drop FPs to ~30 without
|
|
65
|
+
// losing the AWS-key-shaped TP.
|
|
66
|
+
export const DEFAULT_OPTIONS = {
|
|
67
|
+
// Empirical floor for *non-dictionary* credentials. Lower than the
|
|
68
|
+
// 3.5 ceiling Shannon-quoted for "true randomness" because real test
|
|
69
|
+
// fixtures and rotated secrets often use repetitive base alphabets
|
|
70
|
+
// (`abc123abc123...`) at ~2.5 bits/char. Combined with the length and
|
|
71
|
+
// dictionary-token filters, 2.5 catches the Juliet FP class without
|
|
72
|
+
// losing repetitive-pattern fixtures.
|
|
73
|
+
minEntropy: 2.5,
|
|
74
|
+
minLength: 12, // anything shorter is a placeholder / column name
|
|
75
|
+
alphabetMinDistinct: 5, // need at least 5 distinct chars across the value
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Classify a candidate-credential literal value.
|
|
80
|
+
* Returns { skip: true, reason: '<why>' } when this value should be
|
|
81
|
+
* filtered out as a false positive, or { skip: false } when it should
|
|
82
|
+
* proceed to the finding stream.
|
|
83
|
+
*/
|
|
84
|
+
export function classifySecretCandidate(value, opts = {}) {
|
|
85
|
+
if (typeof value !== 'string') return { skip: true, reason: 'non-string-value' };
|
|
86
|
+
const v = value.trim();
|
|
87
|
+
const o = { ...DEFAULT_OPTIONS, ...opts };
|
|
88
|
+
|
|
89
|
+
// Trivial cases — short / dictionary / placeholder.
|
|
90
|
+
if (v.length < o.minLength) return { skip: true, reason: `length<${o.minLength}` };
|
|
91
|
+
if (COMMON_WORDS.has(v.toLowerCase())) return { skip: true, reason: 'common-word' };
|
|
92
|
+
|
|
93
|
+
// Known credential shapes — fast accept.
|
|
94
|
+
if (JWT_RE.test(v)) return { skip: false, reason: 'jwt-shaped' };
|
|
95
|
+
if (HEX_RE.test(v) && v.length >= 32) return { skip: false, reason: 'hex-shaped' };
|
|
96
|
+
if (/^(?:AKIA|ASIA|SK|sk_(?:test|live)_|ghp_|github_pat_|xox[abprs]-|AIza|EAA)[A-Za-z0-9_-]{8,}/.test(v))
|
|
97
|
+
return { skip: false, reason: 'known-provider-prefix' };
|
|
98
|
+
|
|
99
|
+
// Distinct-character count — dictionary words have low diversity.
|
|
100
|
+
const distinct = new Set(v).size;
|
|
101
|
+
if (distinct < o.alphabetMinDistinct) return { skip: true, reason: `distinct<${o.alphabetMinDistinct}` };
|
|
102
|
+
|
|
103
|
+
// Entropy check — the main filter for the Juliet Java FP class.
|
|
104
|
+
const h = shannonEntropy(v);
|
|
105
|
+
if (h < o.minEntropy) return { skip: true, reason: `entropy<${o.minEntropy.toFixed(1)} (${h.toFixed(2)})` };
|
|
106
|
+
|
|
107
|
+
// Word-boundary dictionary check — split on non-alphanumeric and check
|
|
108
|
+
// every token against COMMON_WORDS. Catches "myPasswordChangeme123".
|
|
109
|
+
// All-digit tokens are treated as "common" (likely a placeholder counter
|
|
110
|
+
// rather than meaningful key material).
|
|
111
|
+
const isCommonOrDigits = (t) => COMMON_WORDS.has(t.toLowerCase()) || /^\d+$/.test(t);
|
|
112
|
+
const tokens = v.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
113
|
+
if (tokens.length && tokens.every(isCommonOrDigits)) {
|
|
114
|
+
return { skip: true, reason: 'all-tokens-common-words' };
|
|
115
|
+
}
|
|
116
|
+
// camelCase / PascalCase tokenization: re-split on case transitions so
|
|
117
|
+
// "hardcodedSecret" → ["hardcoded", "Secret"], "myPasswordChangeme"
|
|
118
|
+
// → ["my","Password","Changeme"]. If EVERY camel-token is a common-word
|
|
119
|
+
// we reject the whole value. Same logic as the snake-case path above.
|
|
120
|
+
const camel = v.split(/(?=[A-Z])|[^A-Za-z0-9]+/).filter(Boolean);
|
|
121
|
+
if (camel.length >= 2 && camel.every(isCommonOrDigits)) {
|
|
122
|
+
return { skip: true, reason: 'all-camel-tokens-common-words' };
|
|
123
|
+
}
|
|
124
|
+
// Repetition check — the value is N copies of the same substring. If the
|
|
125
|
+
// repeating unit IS a dictionary word, reject. This catches
|
|
126
|
+
// "passwordpassword" without rejecting "abc123abc123" (the unit isn't a word).
|
|
127
|
+
for (let unit = 2; unit <= v.length / 2; unit++) {
|
|
128
|
+
if (v.length % unit !== 0) continue;
|
|
129
|
+
const head = v.slice(0, unit);
|
|
130
|
+
if (v === head.repeat(v.length / unit) && COMMON_WORDS.has(head.toLowerCase())) {
|
|
131
|
+
return { skip: true, reason: 'repeated-common-word' };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Base64ish with low entropy variant (catches "TestKey1234567" — fixed
|
|
135
|
+
// dictionary content padded with digits).
|
|
136
|
+
if (BASE64ISH_RE.test(v) && h < o.minEntropy + 0.3 && /^[A-Za-z]+/.test(v)) {
|
|
137
|
+
const lead = v.match(/^([A-Za-z]+)/)?.[1] || '';
|
|
138
|
+
if (lead.length >= 4 && COMMON_WORDS.has(lead.toLowerCase())) {
|
|
139
|
+
return { skip: true, reason: 'low-entropy-dictionary-prefix' };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { skip: false, reason: 'passed-filter' };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const _internals = { COMMON_WORDS, BASE64ISH_RE, HEX_RE, JWT_RE };
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// Cross-cloud IAM least-privilege analyzer — Item #4 of the world-class+3 plan.
|
|
2
|
+
//
|
|
3
|
+
// posture/iam-policy.js already covers a tight set of AWS-only over-permissive
|
|
4
|
+
// patterns (wildcard action + wildcard resource on dangerous services). This
|
|
5
|
+
// module fills the cross-cloud gaps:
|
|
6
|
+
//
|
|
7
|
+
// AWS:
|
|
8
|
+
// - aws-public-s3-policy Principal:* on s3:* (object exfil)
|
|
9
|
+
// - aws-public-trust-policy sts:AssumeRole with Principal:* / AWS:*
|
|
10
|
+
// - aws-no-mfa-condition High-risk action without aws:MultiFactorAuthPresent
|
|
11
|
+
// - aws-overbroad-managed-policy AdministratorAccess attached to non-root principal
|
|
12
|
+
// (PassRole-wildcard is detected by posture/iam-policy.js — not duplicated here.)
|
|
13
|
+
//
|
|
14
|
+
// GCP:
|
|
15
|
+
// - gcp-public-iam-binding member 'allUsers' / 'allAuthenticatedUsers'
|
|
16
|
+
// - gcp-owner-binding role 'roles/owner' on non-bootstrap account
|
|
17
|
+
// - gcp-sa-key-export-allowed serviceAccountKeys.create granted broadly
|
|
18
|
+
// - gcp-workload-identity-wildcard pool with broad attribute mapping
|
|
19
|
+
//
|
|
20
|
+
// Azure:
|
|
21
|
+
// - azure-owner-at-sub-scope Owner role assigned at /subscriptions/...
|
|
22
|
+
// - azure-microsoft-auth-wildcard Custom role with Microsoft.Authorization/*
|
|
23
|
+
// - azure-rbac-broad-scope role assignment without scope or principalId tightening
|
|
24
|
+
//
|
|
25
|
+
// File scope (heuristic, no API calls):
|
|
26
|
+
// .json containing Statement / Principal → AWS
|
|
27
|
+
// .yaml / .yml containing bindings: + role: → GCP IAM
|
|
28
|
+
// .json containing "type":"Microsoft.Authorization/roleDefinitions" or
|
|
29
|
+
// .bicep with `resource ... 'Microsoft.Authorization/...'` → Azure
|
|
30
|
+
//
|
|
31
|
+
// Opt-out: AGENTIC_SECURITY_NO_CLOUD_IAM=1
|
|
32
|
+
|
|
33
|
+
import { blankComments } from './_comment-strip.js';
|
|
34
|
+
|
|
35
|
+
function _line(raw, idx) { return raw.slice(0, idx).split('\n').length; }
|
|
36
|
+
function _snip(raw, line) { return (raw.split('\n')[line - 1] || '').trim().slice(0, 200); }
|
|
37
|
+
|
|
38
|
+
function _shape(file, line, ruleId, vuln, fam, sev, cwe, remediation, description, cloud) {
|
|
39
|
+
return {
|
|
40
|
+
id: `${ruleId}:${file}:${line}`,
|
|
41
|
+
file, line, vuln, severity: sev, cwe,
|
|
42
|
+
family: fam, parser: 'CLOUD-IAM',
|
|
43
|
+
confidence: 0.85,
|
|
44
|
+
stride: 'Elevation of Privilege',
|
|
45
|
+
description: description || vuln,
|
|
46
|
+
remediation,
|
|
47
|
+
cloud,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── AWS ────────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
const _HIGH_RISK_AWS_ACTIONS = [
|
|
54
|
+
'iam:*', 'sts:AssumeRole', 'iam:PassRole', 'iam:CreateAccessKey',
|
|
55
|
+
's3:DeleteBucket', 'kms:Decrypt', 'kms:Encrypt', 'kms:GenerateDataKey',
|
|
56
|
+
'rds:DeleteDBInstance', 'ec2:TerminateInstances',
|
|
57
|
+
'secretsmanager:GetSecretValue', 'secretsmanager:PutSecretValue',
|
|
58
|
+
'cloudformation:DeleteStack',
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
function _statements(parsed) {
|
|
62
|
+
const out = [];
|
|
63
|
+
const visit = (node) => {
|
|
64
|
+
if (!node || typeof node !== 'object') return;
|
|
65
|
+
if (Array.isArray(node)) { node.forEach(visit); return; }
|
|
66
|
+
if (node.Statement) {
|
|
67
|
+
const ss = Array.isArray(node.Statement) ? node.Statement : [node.Statement];
|
|
68
|
+
for (const s of ss) out.push(s);
|
|
69
|
+
}
|
|
70
|
+
for (const v of Object.values(node)) {
|
|
71
|
+
if (v && typeof v === 'object') visit(v);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
visit(parsed);
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function _principalIsWildcard(p) {
|
|
79
|
+
if (p === '*') return true;
|
|
80
|
+
if (typeof p === 'object' && p) {
|
|
81
|
+
if (p.AWS === '*' || p.AWS === ['*']) return true;
|
|
82
|
+
if (Array.isArray(p.AWS) && p.AWS.includes('*')) return true;
|
|
83
|
+
if (p['*'] === '*') return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function _actionList(a) {
|
|
89
|
+
if (!a) return [];
|
|
90
|
+
return Array.isArray(a) ? a : [a];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function detectAws(file, raw, out, seen) {
|
|
94
|
+
let parsed;
|
|
95
|
+
try { parsed = JSON.parse(raw); } catch { return; }
|
|
96
|
+
const ss = _statements(parsed);
|
|
97
|
+
for (const s of ss) {
|
|
98
|
+
if ((s.Effect || 'Allow') !== 'Allow') continue;
|
|
99
|
+
const actions = _actionList(s.Action);
|
|
100
|
+
const isStarAction = actions.includes('*') || actions.some(a => /^[a-z]+:\*$/.test(a));
|
|
101
|
+
const hasCondition = s.Condition && Object.keys(s.Condition).length > 0;
|
|
102
|
+
const principalStar = _principalIsWildcard(s.Principal);
|
|
103
|
+
|
|
104
|
+
// aws-public-s3-policy
|
|
105
|
+
if (principalStar && actions.some(a => /^s3:/.test(a))) {
|
|
106
|
+
const ln = _line(raw, `"Principal"`);
|
|
107
|
+
const id = `aws-public-s3-policy:${file}:${ln}`;
|
|
108
|
+
if (!seen.has(id)) {
|
|
109
|
+
seen.add(id);
|
|
110
|
+
out.push(_shape(file, ln, 'aws-public-s3-policy',
|
|
111
|
+
'S3 bucket policy with Principal:* — bucket is publicly accessible',
|
|
112
|
+
'aws-public-s3', 'critical', 'CWE-732',
|
|
113
|
+
'Remove Principal:* and grant the policy to specific AWS account IDs or canonical user IDs. If the bucket genuinely needs to be public (CDN, public website), use CloudFront with an origin access identity instead.',
|
|
114
|
+
'Public bucket policies are the #1 cause of S3 data breaches (Capital One, Verizon, Accenture etc). Even with object ACLs locked down, a permissive bucket policy makes every object world-readable.',
|
|
115
|
+
'aws'));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// aws-public-trust-policy (Principal:* on AssumeRole)
|
|
120
|
+
if (principalStar && actions.includes('sts:AssumeRole')) {
|
|
121
|
+
const ln = _line(raw, 'AssumeRole');
|
|
122
|
+
const id = `aws-public-trust-policy:${file}:${ln}`;
|
|
123
|
+
if (!seen.has(id)) {
|
|
124
|
+
seen.add(id);
|
|
125
|
+
out.push(_shape(file, ln, 'aws-public-trust-policy',
|
|
126
|
+
'IAM role trust policy allows sts:AssumeRole from Principal:* — any AWS account can assume',
|
|
127
|
+
'aws-public-trust', 'critical', 'CWE-863',
|
|
128
|
+
'Restrict the trust policy Principal to specific AWS account IDs or specific AWS services (e.g. lambda.amazonaws.com). Add a Condition on aws:SourceAccount or aws:SourceArn to prevent confused-deputy attacks.',
|
|
129
|
+
'A wildcard trust policy lets anyone with an AWS account assume the role, inheriting all its attached permissions. The 2022 Tesla AWS exposure stemmed from exactly this.',
|
|
130
|
+
'aws'));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// aws-no-mfa-condition on high-risk actions
|
|
135
|
+
for (const a of actions) {
|
|
136
|
+
if (_HIGH_RISK_AWS_ACTIONS.includes(a) || a === '*') {
|
|
137
|
+
const conditionStr = JSON.stringify(s.Condition || {});
|
|
138
|
+
if (!/MultiFactorAuthPresent|MultiFactorAuthAge/.test(conditionStr)) {
|
|
139
|
+
const ln = _line(raw, `"${a}"`);
|
|
140
|
+
const id = `aws-no-mfa-condition:${file}:${a}:${ln}`;
|
|
141
|
+
if (!seen.has(id)) {
|
|
142
|
+
seen.add(id);
|
|
143
|
+
out.push(_shape(file, ln, 'aws-no-mfa-condition',
|
|
144
|
+
`High-risk action ${a} not gated by aws:MultiFactorAuthPresent`,
|
|
145
|
+
'aws-no-mfa', 'high', 'CWE-308',
|
|
146
|
+
'Add a Condition gate: `"Condition": { "Bool": { "aws:MultiFactorAuthPresent": "true" } }`. This forces the calling identity to have authenticated with MFA inside the last hour (configurable via MultiFactorAuthAge).',
|
|
147
|
+
'AWS best-practice and the CIS Benchmark both require MFA-gated sensitive actions. Without it, a compromised long-term access key has root-equivalent power for the policy scope.',
|
|
148
|
+
'aws'));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// (iam:PassRole with Resource:* is detected by posture/iam-policy.js —
|
|
155
|
+
// not duplicated here.)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// aws-overbroad-managed-policy
|
|
159
|
+
if (/"AdministratorAccess"|"PowerUserAccess"/.test(raw) && !/root\b|Bootstrap\b/.test(raw)) {
|
|
160
|
+
const m = /"(AdministratorAccess|PowerUserAccess)"/.exec(raw);
|
|
161
|
+
const ln = _line(raw, m[0]);
|
|
162
|
+
const id = `aws-overbroad-managed-policy:${file}:${ln}`;
|
|
163
|
+
if (!seen.has(id)) {
|
|
164
|
+
seen.add(id);
|
|
165
|
+
out.push(_shape(file, ln, 'aws-overbroad-managed-policy',
|
|
166
|
+
`${m[1]} managed policy attached — overbroad`,
|
|
167
|
+
'aws-overbroad-managed', 'high', 'CWE-269',
|
|
168
|
+
'Replace AdministratorAccess / PowerUserAccess with a least-privilege policy scoped to the resources the principal actually needs. Use AWS IAM Access Analyzer to suggest a narrowed policy from CloudTrail history.',
|
|
169
|
+
'Attaching AdministratorAccess outside the root account is the most common over-permission pattern AWS Trusted Advisor reports. PowerUserAccess is almost as bad (excludes only IAM/Organizations).',
|
|
170
|
+
'aws'));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── GCP ────────────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
function detectGcp(file, raw, out, seen) {
|
|
178
|
+
// YAML or JSON, looking for `bindings` shape.
|
|
179
|
+
// We do a light regex pass — full YAML parsing is heavier than necessary.
|
|
180
|
+
|
|
181
|
+
// gcp-public-iam-binding
|
|
182
|
+
const publicMembers = ['allUsers', 'allAuthenticatedUsers'];
|
|
183
|
+
for (const m of publicMembers) {
|
|
184
|
+
const re = new RegExp(`["']?\\b${m}\\b["']?`, 'g');
|
|
185
|
+
let mm;
|
|
186
|
+
while ((mm = re.exec(raw))) {
|
|
187
|
+
const ln = _line(raw, mm.index);
|
|
188
|
+
const id = `gcp-public-iam-binding:${file}:${m}:${ln}`;
|
|
189
|
+
if (seen.has(id)) continue;
|
|
190
|
+
seen.add(id);
|
|
191
|
+
out.push(_shape(file, ln, 'gcp-public-iam-binding',
|
|
192
|
+
`GCP IAM binding includes ${m} — resource is publicly accessible`,
|
|
193
|
+
'gcp-public-binding', 'critical', 'CWE-732',
|
|
194
|
+
`Remove ${m} from the bindings members list. If the resource must be public (e.g. a GCS object for a public website), explicitly scope it via signed URLs or per-object ACL rather than a project-level IAM binding.`,
|
|
195
|
+
`${m} grants the bound role to anyone with a Google identity (allAuthenticatedUsers) or to literally anyone (allUsers). Has been the source of repeated GCS public-bucket incidents.`,
|
|
196
|
+
'gcp'));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// gcp-owner-binding
|
|
201
|
+
const ownerRe = /role:\s*roles\/owner\b|"role"\s*:\s*"roles\/owner"/g;
|
|
202
|
+
let m;
|
|
203
|
+
while ((m = ownerRe.exec(raw))) {
|
|
204
|
+
const ln = _line(raw, m.index);
|
|
205
|
+
const id = `gcp-owner-binding:${file}:${ln}`;
|
|
206
|
+
if (seen.has(id)) continue;
|
|
207
|
+
seen.add(id);
|
|
208
|
+
out.push(_shape(file, ln, 'gcp-owner-binding',
|
|
209
|
+
'GCP IAM binding grants roles/owner — should be limited to bootstrap accounts',
|
|
210
|
+
'gcp-owner-overuse', 'high', 'CWE-269',
|
|
211
|
+
'Replace roles/owner with the narrowest predefined role (roles/editor, roles/viewer) or a custom role with the specific permissions needed. Roles/owner has the same IAM-mutation power as the project creator.',
|
|
212
|
+
'GCP roles/owner can grant/revoke any other role on the project, including roles/owner itself. Only project bootstrap automation should hold it.',
|
|
213
|
+
'gcp'));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// gcp-sa-key-export-allowed
|
|
217
|
+
if (/iam\.serviceAccountKeys\.create/.test(raw) || /serviceAccountKeyAdmin\b/.test(raw)) {
|
|
218
|
+
const m2 = /serviceAccountKeys\.create|serviceAccountKeyAdmin/.exec(raw);
|
|
219
|
+
const ln = _line(raw, m2.index);
|
|
220
|
+
const id = `gcp-sa-key-export-allowed:${file}:${ln}`;
|
|
221
|
+
if (!seen.has(id)) {
|
|
222
|
+
seen.add(id);
|
|
223
|
+
out.push(_shape(file, ln, 'gcp-sa-key-export-allowed',
|
|
224
|
+
'GCP IAM grants serviceAccountKeys.create — enables long-lived key export',
|
|
225
|
+
'gcp-sa-key-export', 'high', 'CWE-798',
|
|
226
|
+
'Replace user-managed service account keys with Workload Identity Federation (GitHub Actions, AWS, Okta) or Workload Identity for GKE. Long-lived service account keys persist after personnel leaves and are the #1 cause of GCP credential exposure on public repos.',
|
|
227
|
+
'GCP service account keys are the equivalent of AWS access keys but with no built-in rotation. They show up in public GitHub leaks routinely. Workload Identity removes the need for a long-lived secret.',
|
|
228
|
+
'gcp'));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Azure ──────────────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
function detectAzure(file, raw, out, seen) {
|
|
236
|
+
// Detect Azure RBAC role assignments / role definitions in JSON or Bicep.
|
|
237
|
+
|
|
238
|
+
// azure-owner-at-sub-scope
|
|
239
|
+
const ownerRe = /Owner['"]?\s*[,)]|"roleDefinitionId":\s*"[^"]*8e3af657-a8ff-443c-a75c-2fe8c4bcb635/g; // built-in Owner
|
|
240
|
+
let m;
|
|
241
|
+
while ((m = ownerRe.exec(raw))) {
|
|
242
|
+
const ln = _line(raw, m.index);
|
|
243
|
+
// Only flag when the same file references a subscription-scope.
|
|
244
|
+
if (!/\/subscriptions\/[^/]+\/?$|scope:\s*[^,\n]*\/subscriptions\//.test(raw)) continue;
|
|
245
|
+
const id = `azure-owner-at-sub-scope:${file}:${ln}`;
|
|
246
|
+
if (seen.has(id)) continue;
|
|
247
|
+
seen.add(id);
|
|
248
|
+
out.push(_shape(file, ln, 'azure-owner-at-sub-scope',
|
|
249
|
+
'Azure Owner role assigned at subscription scope — full subscription control',
|
|
250
|
+
'azure-owner-sub', 'critical', 'CWE-269',
|
|
251
|
+
'Replace Owner with Contributor + User Access Administrator (split) or a custom role. Subscription Owner can assign roles to itself, recover from any policy, and remove security controls.',
|
|
252
|
+
'Subscription-scope Owner is the highest Azure RBAC role short of Global Admin. CIS Azure Benchmark calls for explicit justification + JIT activation via Azure PIM.',
|
|
253
|
+
'azure'));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// azure-microsoft-auth-wildcard in custom role definitions
|
|
257
|
+
if (/Microsoft\.Authorization\/\*/.test(raw)) {
|
|
258
|
+
const m2 = /Microsoft\.Authorization\/\*/.exec(raw);
|
|
259
|
+
const ln = _line(raw, m2.index);
|
|
260
|
+
const id = `azure-microsoft-auth-wildcard:${file}:${ln}`;
|
|
261
|
+
if (!seen.has(id)) {
|
|
262
|
+
seen.add(id);
|
|
263
|
+
out.push(_shape(file, ln, 'azure-microsoft-auth-wildcard',
|
|
264
|
+
'Custom role grants Microsoft.Authorization/* — enables RBAC self-elevation',
|
|
265
|
+
'azure-auth-wildcard', 'critical', 'CWE-269',
|
|
266
|
+
'Replace Microsoft.Authorization/* with the specific role-management operations the role needs. Microsoft.Authorization/roleAssignments/write is the canonical privilege-escalation primitive in Azure.',
|
|
267
|
+
'Microsoft.Authorization/* permits creating role assignments — meaning the principal can grant itself any role on any scope it reaches. Owner-equivalent in practice.',
|
|
268
|
+
'azure'));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── Entry point ────────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
function _isAwsPolicy(raw) {
|
|
276
|
+
return /"Version"\s*:\s*"2012-10-17"|"Statement"\s*:\s*\[/.test(raw.slice(0, 4000));
|
|
277
|
+
}
|
|
278
|
+
function _isGcpIam(raw) {
|
|
279
|
+
return /\bbindings\s*:|\bgcp[-_]?iam|cloudfunctions\.googleapis|allUsers\b|allAuthenticatedUsers\b/.test(raw);
|
|
280
|
+
}
|
|
281
|
+
function _isAzureIam(raw) {
|
|
282
|
+
return /Microsoft\.Authorization|roleDefinitions|roleAssignments|azurerm_role_assignment/.test(raw);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function scanCloudIam(fp, raw) {
|
|
286
|
+
if (process.env.AGENTIC_SECURITY_NO_CLOUD_IAM === '1') return [];
|
|
287
|
+
if (!raw || raw.length > 500_000) return [];
|
|
288
|
+
const out = [];
|
|
289
|
+
const seen = new Set();
|
|
290
|
+
const isJson = /\.json$/i.test(fp) || raw.trimStart().startsWith('{');
|
|
291
|
+
const isYaml = /\.(?:yaml|yml)$/i.test(fp);
|
|
292
|
+
const isBicep = /\.bicep$/i.test(fp);
|
|
293
|
+
const isTf = /\.tf$/i.test(fp);
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
if ((isJson || isTf) && _isAwsPolicy(raw)) detectAws(fp, raw, out, seen);
|
|
297
|
+
} catch {}
|
|
298
|
+
try {
|
|
299
|
+
if ((isYaml || isJson) && _isGcpIam(raw)) detectGcp(fp, raw, out, seen);
|
|
300
|
+
} catch {}
|
|
301
|
+
try {
|
|
302
|
+
if ((isJson || isBicep || isTf) && _isAzureIam(raw)) detectAzure(fp, raw, out, seen);
|
|
303
|
+
} catch {}
|
|
304
|
+
|
|
305
|
+
for (const f of out) f.file = fp;
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export const _internals = {
|
|
310
|
+
_statements, _principalIsWildcard, _isAwsPolicy, _isGcpIam, _isAzureIam,
|
|
311
|
+
detectAws, detectGcp, detectAzure, _HIGH_RISK_AWS_ACTIONS,
|
|
312
|
+
};
|
package/src/sast/cpp.js
CHANGED
|
@@ -81,6 +81,33 @@ function _isStrcpyGuarded(ctx) {
|
|
|
81
81
|
return _SIZEOF_GUARD_RE.test(window);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// Destination-size guard — Recommendation #6 of the SCA/SAST improvement
|
|
85
|
+
// plan. Read the first arg of a strcpy/strcat/sprintf call site and look
|
|
86
|
+
// backwards in the function for a `char dest[N];` declaration. If found,
|
|
87
|
+
// classify the buffer as "small" (≤ 256 bytes) or "large" (> 256) — the
|
|
88
|
+
// large case suggests the developer sized intentionally and we downgrade
|
|
89
|
+
// to medium confidence; the small case keeps the high-confidence finding.
|
|
90
|
+
// If no fixed-size declaration is found at all (heap-allocated, struct
|
|
91
|
+
// member, function parameter), we keep the original finding shape.
|
|
92
|
+
const _CHAR_BUFFER_DECL_RE = /\b(?:char|unsigned\s+char|signed\s+char|wchar_t|int8_t|uint8_t)\s+(\w+)\s*\[\s*(\d+|\w+)\s*\]\s*;/g;
|
|
93
|
+
function _classifyDestBuffer(ctx, destName) {
|
|
94
|
+
if (!destName) return { kind: 'unknown' };
|
|
95
|
+
const lines = ctx.raw.split('\n');
|
|
96
|
+
// Walk backwards from the current line; cap at file start.
|
|
97
|
+
const before = lines.slice(0, ctx.line).join('\n');
|
|
98
|
+
let bestSize = null;
|
|
99
|
+
let m;
|
|
100
|
+
const re = new RegExp(_CHAR_BUFFER_DECL_RE.source, 'g');
|
|
101
|
+
while ((m = re.exec(before))) {
|
|
102
|
+
if (m[1] !== destName) continue;
|
|
103
|
+
const sizeTxt = m[2];
|
|
104
|
+
if (/^\d+$/.test(sizeTxt)) bestSize = parseInt(sizeTxt, 10);
|
|
105
|
+
}
|
|
106
|
+
if (bestSize === null) return { kind: 'unknown' };
|
|
107
|
+
if (bestSize <= 256) return { kind: 'small-fixed', size: bestSize };
|
|
108
|
+
return { kind: 'large-fixed', size: bestSize };
|
|
109
|
+
}
|
|
110
|
+
|
|
84
111
|
// Format-string: only fire when the variable holding the format string was
|
|
85
112
|
// not assigned from a string literal earlier in the file.
|
|
86
113
|
function _isPrintfVarLiteral(ctx, varName) {
|
|
@@ -99,10 +126,21 @@ const FINDINGS = [
|
|
|
99
126
|
// variants on Windows and strlcpy on BSD/macOS.
|
|
100
127
|
{
|
|
101
128
|
id: 'cpp-strcpy', severity: 'high', cwe: 'CWE-120', family: 'buffer-overflow',
|
|
102
|
-
|
|
129
|
+
// Capture the destination identifier so we can apply the destination-size
|
|
130
|
+
// guard (Recommendation #6) — large fixed buffers downgrade severity.
|
|
131
|
+
re: /\b(strcpy|strcat|gets|stpcpy|sprintf)\s*\(\s*(\w+)/g,
|
|
103
132
|
vuln: 'Banned API — unbounded string copy/format (potential buffer overflow)',
|
|
104
133
|
remediation: 'Replace with the bounded variant: strcpy → strlcpy / strcpy_s; strcat → strlcat / strcat_s; gets → fgets(buf, sizeof(buf), stdin); sprintf → snprintf(buf, sizeof(buf), "%s", v). The unbounded form will silently overflow on attacker-controlled input.',
|
|
105
|
-
gate: (ctx) =>
|
|
134
|
+
gate: (ctx, m) => {
|
|
135
|
+
if (_isStrcpyGuarded(ctx)) return false;
|
|
136
|
+
// Destination classification — large fixed buffers are intentional
|
|
137
|
+
// and we demote them to a less-noisy emission. Small fixed buffers
|
|
138
|
+
// stay high-severity; unknown (heap / param) keeps the original behavior.
|
|
139
|
+
const destName = m && m[2];
|
|
140
|
+
ctx._destClass = _classifyDestBuffer(ctx, destName);
|
|
141
|
+
return true;
|
|
142
|
+
},
|
|
143
|
+
severityFor: (ctx) => ctx._destClass && ctx._destClass.kind === 'large-fixed' ? 'medium' : 'high',
|
|
106
144
|
},
|
|
107
145
|
{
|
|
108
146
|
// printf/warn-family: format string is ARG 1.
|
|
@@ -196,8 +234,97 @@ const FINDINGS = [
|
|
|
196
234
|
// common (bad) example pattern, not a real vulnerability.
|
|
197
235
|
gate: (ctx) => _isCryptoContextRand(ctx),
|
|
198
236
|
},
|
|
237
|
+
|
|
238
|
+
// ── Recommendation #6/7: C/C++ family expansion ──────────────────────────
|
|
239
|
+
|
|
240
|
+
// exec*-family with non-literal argument (CWE-78). Beyond system/popen.
|
|
241
|
+
{
|
|
242
|
+
id: 'cpp-exec-family', severity: 'critical', cwe: 'CWE-78', family: 'command-injection',
|
|
243
|
+
re: /\b(?:execl|execle|execlp|execlpe|execv|execve|execvp|execvpe|posix_spawn)\s*\(\s*(?!["'])\w+/g,
|
|
244
|
+
vuln: 'Command Injection — exec*() family with non-literal program path',
|
|
245
|
+
remediation: 'Pin the program path to a constant and pass arguments as a separate argv. Never pass user-controlled data as the program path itself; an attacker can substitute any binary on $PATH.',
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
// Weak crypto — OpenSSL legacy / EVP_des / MD5 / SHA1 (CWE-327).
|
|
249
|
+
{
|
|
250
|
+
id: 'cpp-weak-crypto-md', severity: 'high', cwe: 'CWE-327', family: 'weak-crypto',
|
|
251
|
+
re: /\b(?:MD5_(?:Init|Update|Final|MD5)|MD4_|MD2_|SHA1_(?:Init|Update|Final|SHA1)|RIPEMD160_)\s*\(/g,
|
|
252
|
+
vuln: 'Weak Cryptography — legacy MD5/MD4/SHA1/RIPEMD160 hash primitive',
|
|
253
|
+
remediation: 'Use SHA-256 or SHA-3 via the OpenSSL EVP interface (`EVP_sha256()` → `EVP_DigestInit_ex` → `EVP_DigestUpdate` → `EVP_DigestFinal_ex`). For password hashing use Argon2 (libsodium) or scrypt.',
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: 'cpp-weak-crypto-des', severity: 'high', cwe: 'CWE-327', family: 'weak-crypto',
|
|
257
|
+
re: /\b(?:DES_(?:set_key|ecb_encrypt|ncbc_encrypt|cbc_encrypt|ede3_cbc_encrypt)|RC2_|RC4_(?:set_key|encrypt|decrypt)|BF_(?:set_key|ecb_encrypt))\s*\(/g,
|
|
258
|
+
vuln: 'Weak Cryptography — legacy DES/3DES/RC2/RC4/Blowfish primitive',
|
|
259
|
+
remediation: 'Use AES-256 in GCM mode via the EVP interface (`EVP_aes_256_gcm()` → `EVP_EncryptInit_ex`). DES and RC4 are broken; 3DES is deprecated; Blowfish has a 64-bit block (Sweet32).',
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: 'cpp-weak-crypto-evp', severity: 'high', cwe: 'CWE-327', family: 'weak-crypto',
|
|
263
|
+
re: /\bEVP_(?:des_(?:ede3?)?_(?:cbc|ecb|cfb|ofb)|md5|md4|md2|sha1|rc4|rc2_(?:cbc|ecb|cfb|ofb)|bf_(?:cbc|ecb|cfb|ofb))\s*\(/g,
|
|
264
|
+
vuln: 'Weak Cryptography — EVP factory for legacy primitive (MD5/SHA1/DES/3DES/RC2/RC4/BF)',
|
|
265
|
+
remediation: 'Use a modern EVP factory: `EVP_aes_256_gcm()` for AEAD, `EVP_sha256()` / `EVP_sha3_256()` for hashing.',
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
// Hardcoded secret in C/C++ (CWE-798) — a string literal assigned to a
|
|
269
|
+
// variable matching credential naming. Same idea as the Java/JS detector
|
|
270
|
+
// but tuned to C idioms.
|
|
271
|
+
{
|
|
272
|
+
id: 'cpp-hardcoded-secret', severity: 'high', cwe: 'CWE-798', family: 'hardcoded-secret',
|
|
273
|
+
// `static const char *password = "literal";` or `#define PASSWORD "literal"`
|
|
274
|
+
re: /\b(?:const\s+)?char\s*(?:\*\s*)?(?:const\s+)?(\w*(?:password|passwd|pwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|cred(?:ential)?s?|priv(?:ate)?[_-]?key)\w*)\s*\[?\s*\]?\s*=\s*"([^"]{8,})"/gi,
|
|
275
|
+
vuln: 'Hardcoded Secret — credential-named char assigned a string literal',
|
|
276
|
+
remediation: 'Load secrets at runtime from environment variables (`getenv("API_KEY")`), a secrets file with restricted permissions, or a vault SDK. Never compile a literal credential into the binary — `strings(1)` extracts every literal string and the binary ships with the secret embedded.',
|
|
277
|
+
gate: (ctx, m) => {
|
|
278
|
+
// Apply the same entropy filter to avoid the 468 FPs/1 TP problem.
|
|
279
|
+
const val = m && m[2];
|
|
280
|
+
if (!val) return false;
|
|
281
|
+
try {
|
|
282
|
+
// Lazy import — avoids a circular dep on the entropy module being
|
|
283
|
+
// present in older snapshots.
|
|
284
|
+
// eslint-disable-next-line no-unused-vars
|
|
285
|
+
const { classifySecretCandidate } = _entropyMod || {};
|
|
286
|
+
if (classifySecretCandidate) {
|
|
287
|
+
const r = classifySecretCandidate(val);
|
|
288
|
+
if (r.skip) return false;
|
|
289
|
+
}
|
|
290
|
+
} catch { /* fail open */ }
|
|
291
|
+
return true;
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
// Use-after-free / double-free — CWE-416 / CWE-415. Heuristic: same pointer
|
|
296
|
+
// referenced in a free() call earlier in the function, then dereferenced
|
|
297
|
+
// (or free'd again) later. Conservative on declared `nullptr`-after-free.
|
|
298
|
+
{
|
|
299
|
+
id: 'cpp-uaf-heuristic', severity: 'high', cwe: 'CWE-416', family: 'mem-unsafe',
|
|
300
|
+
re: /\bfree\s*\(\s*(\w+)\s*\)\s*;[\s\S]{0,400}?\b\1\s*(?:->|\[|=\s*[^=])/g,
|
|
301
|
+
vuln: 'Use-After-Free heuristic — free(p) followed by p->/p[ access in same function window',
|
|
302
|
+
remediation: 'After `free(p)`, set `p = NULL;` immediately. The compiler can\'t catch UAF in general; explicit nulling means later derefs crash on a null check instead of executing on freed memory.',
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
id: 'cpp-double-free', severity: 'high', cwe: 'CWE-415', family: 'mem-unsafe',
|
|
306
|
+
re: /\bfree\s*\(\s*(\w+)\s*\)\s*;[\s\S]{0,400}?\bfree\s*\(\s*\1\s*\)/g,
|
|
307
|
+
vuln: 'Double-free — free(p) followed by free(p) without nulling in between',
|
|
308
|
+
remediation: 'After `free(p)`, set `p = NULL;`. `free(NULL)` is a defined no-op in C; `free(already_freed_p)` is undefined behavior that can corrupt the allocator and pivot to RCE.',
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
// Cookie / Session / token = rand() shape — when rand() is used to mint
|
|
312
|
+
// session identifiers, even outside a strict "crypto" context. Distinct
|
|
313
|
+
// from the gated rand() rule above to catch the obvious Juliet shape.
|
|
314
|
+
{
|
|
315
|
+
id: 'cpp-rand-session-token', severity: 'high', cwe: 'CWE-338', family: 'weak-rng',
|
|
316
|
+
re: /\b(?:session_id|token|cookie|nonce|csrf|secret_key)\s*(?:=|\.|->)[\s\S]{0,200}?\b(?:rand|random)\s*\(/gi,
|
|
317
|
+
vuln: 'Weak Randomness — session/token/cookie value derived from rand()/random()',
|
|
318
|
+
remediation: 'Use getrandom() (Linux), RAND_bytes() (OpenSSL), or BCryptGenRandom() (Windows) for any identifier that has to be unguessable. rand() outputs are predictable to within 2^31 internal states.',
|
|
319
|
+
},
|
|
199
320
|
];
|
|
200
321
|
|
|
322
|
+
// Late-bound entropy module — imported via a dynamic require shim so the
|
|
323
|
+
// rule table can lazy-call it from inside gate functions without creating
|
|
324
|
+
// a circular import at module load time.
|
|
325
|
+
let _entropyMod = null;
|
|
326
|
+
import('./_secret-entropy.js').then(m => { _entropyMod = m; }).catch(() => { _entropyMod = null; });
|
|
327
|
+
|
|
201
328
|
function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
|
|
202
329
|
|
|
203
330
|
export function scanCpp(fp, raw) {
|
|
@@ -223,15 +350,22 @@ export function scanCpp(fp, raw) {
|
|
|
223
350
|
if (/^\s*#\s*define\b/.test(lineText)) continue;
|
|
224
351
|
// Per-rule contextual gate (Action 2). Suppress when the surrounding
|
|
225
352
|
// file/line context shows the call is not security-relevant.
|
|
353
|
+
const gateCtx = { file: fp, raw, line, lineText };
|
|
226
354
|
if (typeof rule.gate === 'function') {
|
|
227
355
|
try {
|
|
228
|
-
if (!rule.gate(
|
|
356
|
+
if (!rule.gate(gateCtx, m)) continue;
|
|
229
357
|
} catch { /* gate threw → fail open, keep finding */ }
|
|
230
358
|
}
|
|
359
|
+
// Per-finding severity override (Recommendation #6 — destination-size
|
|
360
|
+
// classifier downgrades large-fixed-buffer strcpy to medium).
|
|
361
|
+
let sev = rule.severity;
|
|
362
|
+
if (typeof rule.severityFor === 'function') {
|
|
363
|
+
try { sev = rule.severityFor(gateCtx, m) || sev; } catch { /* keep default */ }
|
|
364
|
+
}
|
|
231
365
|
out.push({
|
|
232
366
|
id, file: fp, line,
|
|
233
367
|
vuln: rule.vuln,
|
|
234
|
-
severity:
|
|
368
|
+
severity: sev,
|
|
235
369
|
cwe: rule.cwe,
|
|
236
370
|
stride: rule.family === 'buffer-overflow' || rule.family === 'mem-unsafe' ? 'Tampering'
|
|
237
371
|
: rule.family === 'command-injection' ? 'Elevation of Privilege'
|