@clear-capabilities/agentic-security-scanner 0.153.1 → 0.153.2

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.
@@ -1 +1 @@
1
- 1e727ef20f7fd2c897af4598b51db75771cc0b75e07dd00a30bd840d34736b10 agentic-security.mjs
1
+ dce16625abaef7a52aad4a8d0ca0402d22ea681c9a1479f7378401458e38d55e agentic-security.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clear-capabilities/agentic-security-scanner",
3
- "version": "0.153.1",
3
+ "version": "0.153.2",
4
4
  "description": "Scanner engine for the agentic-security Claude Code plugin — SAST, SCA (function-level reachability + CISA KEV), secrets, IaC, prompt-injection, MCP/agent-tool audit, auth/authZ deep analysis, attack chains, PoC generation, business logic, toxic-combinations scoring, SBOM, pipeline integrity, compliance attestation, and more.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -52,7 +52,7 @@
52
52
  "z3-solver": "^5.2.0"
53
53
  },
54
54
  "devDependencies": {
55
- "@types/node": "^26.5.1",
55
+ "@types/node": "^26.6.4",
56
56
  "@vercel/ncc": "^0.45.0",
57
57
  "java-parser": "^3.0.1"
58
58
  },
package/src/engine.js CHANGED
@@ -5700,19 +5700,29 @@ function _isLikelyUnsafeRegex(body){
5700
5700
  // the first required literal). redos-nfa.js is consulted as a second
5701
5701
  // opinion for the intrinsic case only, since it does not model the
5702
5702
  // polynomial one.
5703
- if (!_hasNestedQuantifier(body) && !_hasLeadingUnboundedQuantifier(body)) return false;
5703
+ const ambiguousAlt = _hasOverlappingQuantifiedAlternation(body);
5704
+ if (!_hasNestedQuantifier(body) && !_hasLeadingUnboundedQuantifier(body) && !ambiguousAlt) return false;
5704
5705
  // FIX-DISCRIMINATION (GHSA-29g2-3rmr-qm68): an anchored pattern with no
5705
5706
  // nested quantifier has neither failure mode — no start-offset multiplier,
5706
5707
  // no intrinsic catastrophe. safe-regex's star-height heuristic cannot see
5707
5708
  // anchoring at all and flagged the fixed revision identically to the
5708
5709
  // vulnerable one, so the finding survived its own fix.
5709
- if (_isAnchoredRegex(body) && !_hasNestedQuantifier(body)) return false;
5710
+ if (_isAnchoredRegex(body) && !_hasNestedQuantifier(body) && !ambiguousAlt) return false;
5710
5711
  if (_safeRegex) {
5711
5712
  try { if (!_safeRegex(body)) return true; } catch (_) { /* fall through */ }
5712
5713
  }
5713
- // safe-regex's star-height analysis misses `(a|aa)*` — the alternatives
5714
- // overlap on their first character, which causes catastrophic backtracking
5715
- // even at star-height 1. Add a first-char overlap check on `(X|Y…)[*+]` shapes.
5714
+ // safe-regex's star-height analysis misses `(a|aa)*` (see the helper below).
5715
+ return ambiguousAlt;
5716
+ }
5717
+ /**
5718
+ * `(X|Y…)[*+]` whose alternatives can start with the same character — `(a|aa)*`.
5719
+ * The overlap makes the engine fork on every iteration, which is catastrophic
5720
+ * backtracking even at star-height 1, and like a nested quantifier it is
5721
+ * intrinsic: anchoring does not cure it. A third mechanism alongside nested
5722
+ * quantifiers and scan-and-retry; the gate in _isLikelyUnsafeRegex must admit
5723
+ * it, or `/^(a|aa)*$/` is rejected before this check is ever reached.
5724
+ */
5725
+ function _hasOverlappingQuantifiedAlternation(body){
5716
5726
  const altQuantRe = /\((?:\?:)?([^()]+)\)[*+]/g;
5717
5727
  let am;
5718
5728
  while ((am = altQuantRe.exec(body)) !== null) {
@@ -123,6 +123,20 @@ function _lang(fp) {
123
123
  return null;
124
124
  }
125
125
 
126
+ // True when `idx` sits inside a '…' or "…" literal on its own line. A sink name
127
+ // quoted in a string (a rule library's `fix:`/`example:` text, a log message) is
128
+ // prose, not a call. Backticks are deliberately not tracked: a template literal's
129
+ // `${…}` holds real code, and erring toward "this is code" is the safe direction.
130
+ function _insideQuote(code, idx) {
131
+ let q = null;
132
+ for (let i = code.lastIndexOf('\n', idx - 1) + 1; i < idx; i++) {
133
+ const c = code[i];
134
+ if (q) { if (c === '\\') i++; else if (c === q) q = null; }
135
+ else if (c === '\'' || c === '"') q = c;
136
+ }
137
+ return q !== null;
138
+ }
139
+
126
140
  export function scanCodeInjectionMultilang(fp, raw) {
127
141
  if (!raw || raw.length > 500_000) return [];
128
142
  const lang = _lang(fp);
@@ -137,6 +151,7 @@ export function scanCodeInjectionMultilang(fp, raw) {
137
151
  const r = new RegExp(re.source, re.flags);
138
152
  let m;
139
153
  while ((m = r.exec(code))) {
154
+ if (_insideQuote(code, m.index + Math.max(0, m[0].search(/\w/)))) continue;
140
155
  const line = lineOf(code, m.index);
141
156
  const id = `code-injection-ml:${fp}:${line}:${key}`;
142
157
  if (seen.has(id)) continue;
@@ -5,6 +5,9 @@
5
5
  // - exec.Command shell-form exec.Command("sh", "-c", <var>) vs argv-form
6
6
  // - http.Client custom-transport http.Get(<var>) or http.NewRequest with
7
7
  // user-controlled URL and no allowlist
8
+ // - package-level http.Get/Post runs on http.DefaultClient, which has no
9
+ // timeout (DoS: one stalled upstream pins
10
+ // the goroutine and its connection forever)
8
11
  //
9
12
  // These patterns are narrow and complement the existing regex-based Go rules
10
13
  // (GORM raw SQL, source patterns for net/http, Echo, Chi, Gin).
@@ -38,6 +41,14 @@ const FINDINGS = [
38
41
  vuln: 'SSRF — http.Get/Post with variable URL',
39
42
  remediation: 'Allowlist the destination host before any net/http call. Use `net/url.Parse(target)` then check `parsed.Host` against an explicit allowlist. Reject RFC1918 (10/8, 172.16/12, 192.168/16) and the cloud metadata addresses (169.254.169.254, fd00:ec2::254).',
40
43
  },
44
+ {
45
+ id: 'go-http-no-timeout', severity: 'medium', cwe: 'CWE-400', family: 'dos-no-timeout',
46
+ // Only the package-level helpers. `client.Get(...)` is the caller's own
47
+ // client, which may well set Timeout, so it is deliberately not matched.
48
+ re: /\bhttp\.(?:Get|Post|Head|PostForm)\s*\(/g,
49
+ vuln: 'Missing Timeout on Outbound HTTP Request (DoS)',
50
+ remediation: 'http.Get/Post/Head/PostForm use http.DefaultClient, which never times out. Build a client instead: `c := &http.Client{Timeout: 10 * time.Second}` and call `c.Get(...)`, or pass a context with a deadline to http.NewRequestWithContext.',
51
+ },
41
52
  {
42
53
  id: 'go-newrequest-user-url', severity: 'high', cwe: 'CWE-918', family: 'ssrf',
43
54
  re: /\bhttp\.NewRequest(?:WithContext)?\s*\(\s*[^,]+,\s*(?!"[^"]*"\s*[,)])[a-zA-Z_]\w*/g,