@clear-capabilities/agentic-security-scanner 0.195.11 → 0.195.13
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 +32 -0
- package/bin/agentic-security.js +6 -0
- package/dist/{13e6d1463f0aec440afa.json → 1f01fe10027e772f73a1.json} +3 -3
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +3 -3
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/catalog-expanded.js +5 -1
- package/src/dataflow/engine.js +7 -0
- package/src/language/CLAUDE.md +2 -0
- package/src/language/haskell-guards.js +10 -0
- package/src/language/haskell-ir.js +118 -2
- package/src/language/haskell-models.js +5 -0
- package/src/language/haskell-parser.js +15 -1
- package/src/language/haskell-syntax.js +44 -6
- package/src/posture/CLAUDE.md +5 -0
- package/src/posture/api-contract.js +39 -1
- package/src/posture/corpus-match.js +12 -0
|
@@ -185,9 +185,47 @@ export function runApiContractScan(scanRoot, codeRoutes) {
|
|
|
185
185
|
if (c.kind === 'openapi') contractRoutes = parseOpenAPI(c.doc);
|
|
186
186
|
else if (c.kind === 'graphql') contractRoutes = parseGraphQL(c.doc);
|
|
187
187
|
contractRoutes.forEach(r => { r.contractFile = c.path; });
|
|
188
|
-
findings.push(...diffRoutes(contractRoutes, codeRoutes || []));
|
|
188
|
+
findings.push(...diffRoutes(contractRoutes, routesGovernedBy(c.path, scanRoot, codeRoutes || [])));
|
|
189
189
|
}
|
|
190
190
|
return findings;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
// A manifest that makes a directory a package of its own: the contract found inside such a package describes that package's API.
|
|
194
|
+
const PACKAGE_MANIFEST = /^(?:package\.json|pyproject\.toml|setup\.py|setup\.cfg|go\.mod|pom\.xml|build\.gradle(?:\.kts)?|Cargo\.toml|composer\.json|Gemfile|mix\.exs|package\.yaml|[^/]+\.cabal|[^/]+\.csproj|[^/]+\.fsproj)$/i;
|
|
195
|
+
|
|
196
|
+
function hasPackageManifest(dir) {
|
|
197
|
+
try { return fs.readdirSync(dir).some((n) => PACKAGE_MANIFEST.test(n)); } catch { return false; }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The directory a contract file governs: the nearest ancestor of the file (up to the scan root) that holds a package manifest. A contract at
|
|
202
|
+
* the scan root's own package, or in a directory with no package above it, governs every route as before.
|
|
203
|
+
*
|
|
204
|
+
* Why: a monorepo's packages each carry their own API; an example `swagger.json` three packages away does not declare the routes of the
|
|
205
|
+
* others. Scanned over a servant monorepo, two example contracts in `servant-swagger/example` and `servant-openapi3/example` produced 78
|
|
206
|
+
* "implemented but not declared" routes across every other package, each twice. Scoping by package keeps the single-package case unchanged.
|
|
207
|
+
*/
|
|
208
|
+
export function contractScope(contractPath, scanRoot) {
|
|
209
|
+
const top = path.resolve(scanRoot);
|
|
210
|
+
let dir = path.dirname(path.resolve(contractPath));
|
|
211
|
+
while (true) {
|
|
212
|
+
if (dir === top) return top;
|
|
213
|
+
if (!dir.startsWith(top + path.sep)) return top; // outside the scan root: no narrower scope can be claimed
|
|
214
|
+
if (hasPackageManifest(dir)) return dir;
|
|
215
|
+
const up = path.dirname(dir);
|
|
216
|
+
if (up === dir) return top;
|
|
217
|
+
dir = up;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function routesGovernedBy(contractPath, scanRoot, codeRoutes) {
|
|
222
|
+
const scope = contractScope(contractPath, scanRoot);
|
|
223
|
+
if (scope === path.resolve(scanRoot)) return codeRoutes;
|
|
224
|
+
return codeRoutes.filter((r) => {
|
|
225
|
+
if (!r || typeof r.file !== 'string') return false;
|
|
226
|
+
const abs = path.resolve(scanRoot, r.file);
|
|
227
|
+
return abs === scope || abs.startsWith(scope + path.sep);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
193
231
|
export const _internals = { CONTRACT_FILE_PATTERNS, parseOpenAPI, parseGraphQL };
|
|
@@ -47,9 +47,21 @@ function _any(scan, predicate) {
|
|
|
47
47
|
return false;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// A finding the scanner itself has PROVED clean (`proof.verdict` `proven-*`: the sink is reached only through a sanitizer that covers the
|
|
51
|
+
// finding's class, or only through a parameterizer) is a refuted report, not a detection: the fix lifecycle already drops it and the report
|
|
52
|
+
// marks it refuted. It scores on neither side, so a fix that sanitises through a recognised escaper reads as `post:TN` and a vulnerable
|
|
53
|
+
// shape the engine wrongly proves clean reads as `pre:FN`, both the honest direction. Added for the first real-advisory Haskell entry (keter,
|
|
54
|
+
// HSEC-2024-0001, whose fix is an HTML escaper); the full baseline was re-run and no synthetic entry's verdict moved.
|
|
55
|
+
function _refuted(f) { return !!(f && f.proof && typeof f.proof.verdict === 'string' && f.proof.verdict.startsWith('proven-')); }
|
|
56
|
+
// A finding at `info` with a guard note is one the engine lowered on evidence it read (a non-HTML content type, a redirect body, a fixed
|
|
57
|
+
// host): kept in the report so the lowering is visible, not a vulnerability it reports. It scores on neither side either. An `info`
|
|
58
|
+
// finding WITHOUT such a note (a rule that is informational by design) still scores, so no synthetic entry's verdict moves.
|
|
59
|
+
function _loweredOnEvidence(f) { return !!(f && f.severity === 'info' && f.guardNote && typeof f.guardNote === 'object'); }
|
|
60
|
+
|
|
50
61
|
// One predicate, both sides. See the header for why the symmetric form is the
|
|
51
62
|
// LOOSE one.
|
|
52
63
|
function _matches(f, manifest, matcher) {
|
|
64
|
+
if (_refuted(f) || _loweredOnEvidence(f)) return false;
|
|
53
65
|
return (matcher.test(f.vuln || '') || matcher.test(f.family || '')) &&
|
|
54
66
|
(manifest?.cwe ? f.cwe === manifest.cwe || matcher.test(f.cwe || '') : true);
|
|
55
67
|
}
|