@clear-capabilities/agentic-security-scanner 0.157.2 → 0.159.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/1919.index.js +5 -2
  3. package/dist/2862.index.js +29 -9
  4. package/dist/4171.index.js +2 -2
  5. package/dist/{8551.index.js → 7033.index.js} +430 -22
  6. package/dist/9560.index.js +3 -1
  7. package/dist/agentic-security.mjs +15 -15
  8. package/dist/agentic-security.mjs.sha256 +1 -1
  9. package/package.json +7 -5
  10. package/src/dataflow/engine.js +23 -0
  11. package/src/engine.js +7 -1
  12. package/src/language/CLAUDE.md +53 -1
  13. package/src/language/assurance.js +21 -4
  14. package/src/language/context.js +2 -0
  15. package/src/language/cvss.js +91 -0
  16. package/src/language/fix-lifecycle.js +114 -21
  17. package/src/language/haskell-adapter.js +4 -3
  18. package/src/language/haskell-advisory-feed.js +194 -0
  19. package/src/language/haskell-boundaries.js +220 -0
  20. package/src/language/haskell-cpp.js +460 -0
  21. package/src/language/haskell-ir.js +6 -3
  22. package/src/language/haskell-models.js +12 -5
  23. package/src/language/haskell-parser.js +122 -38
  24. package/src/language/haskell-sca.js +28 -10
  25. package/src/language/haskell-security-rules.js +1 -1
  26. package/src/language/haskell-supply.js +70 -8
  27. package/src/language/haskell-web.js +143 -10
  28. package/src/language/nix-advisory-feed.js +275 -0
  29. package/src/language/nix-fix.js +162 -24
  30. package/src/language/nix-ir.js +16 -12
  31. package/src/language/nix-sca.js +80 -15
  32. package/src/language/nixos-eval-forms.js +162 -0
  33. package/src/language/nixos-hardening.js +37 -2
  34. package/src/language/nixos-module-resolver.js +359 -39
  35. package/src/language/nixos-option-catalog.js +1 -0
  36. package/src/language/resolved-pass.js +65 -3
  37. package/src/lsp/server.js +5 -2
  38. package/src/mcp/tools.js +3 -1
  39. package/src/posture/fix-history.js +30 -3
  40. package/src/report/index.js +4 -1
  41. package/dist/1310.index.js +0 -3161
  42. package/dist/2376.index.js +0 -3151
  43. package/dist/3526.index.js +0 -1019
  44. package/dist/7310.index.js +0 -520
  45. package/dist/8520.index.js +0 -13
  46. package/dist/9091.index.js +0 -3135
package/src/mcp/tools.js CHANGED
@@ -1079,7 +1079,9 @@ export const synthesize_fix = {
1079
1079
  const proj = lc.loadLanguageProject(ctx.sessionRoot);
1080
1080
  const prev = await lc.languageFixPreview(f, proj.files);
1081
1081
  languageFix = { status: prev.status, ok: prev.ok, label: prev.label || null, tier: prev.tier || null, reason: prev.reason || null, diff: prev.diff || null, explanation: prev.explanation || null, consequences: prev.consequences || [] };
1082
- if (prev.ok) autofix = { deterministic: true, ruleId: f.rule || f.family || null, patch: prev.after, file: prev.file, label: prev.label || null, verified: true };
1082
+ // apply_fix writes ONE file; a fix that spans several files must go through the CLI lifecycle (atomic, grouped undo).
1083
+ if (prev.ok && prev.edits) languageFix.multiFile = prev.edits.map((e) => e.file);
1084
+ else if (prev.ok) autofix = { deterministic: true, ruleId: f.rule || f.family || null, patch: prev.after, file: prev.file, label: prev.label || null, verified: true };
1083
1085
  }
1084
1086
  } catch { /* best-effort: the preview is advisory */ }
1085
1087
  }
@@ -362,7 +362,7 @@ export async function applyFix({ scanRoot, file, originalContent, newContent, fi
362
362
  * MITIGATION, WORKAROUND) and the gates it passed are kept on the entry beside the timestamp. No second write happens
363
363
  * here; `backup.original` is the file the lifecycle saved before it wrote.
364
364
  */
365
- export async function recordExternalFix({ scanRoot, file, originalContent, newContent, findingId, ruleId, vuln, stableId, fixLabel = null, verification = null, backup = null, source = 'language-lifecycle', findingProvenance = null }) {
365
+ export async function recordExternalFix({ scanRoot, file, originalContent, newContent, findingId, ruleId, vuln, stableId, fixLabel = null, verification = null, backup = null, source = 'language-lifecycle', findingProvenance = null, languageGroupId = null }) {
366
366
  return _withLogLock(scanRoot, async () => {
367
367
  if (!ensure(scanRoot)) return null;
368
368
  const log = readLog(scanRoot);
@@ -376,6 +376,7 @@ export async function recordExternalFix({ scanRoot, file, originalContent, newCo
376
376
  appliedAt: new Date().toISOString(), status: 'applied', reverted: false, attemptOrdinal: priorAttempts + 1,
377
377
  provenanceAtFix: _snapshotProvenanceAtFix(findingProvenance, new Date().toISOString()),
378
378
  fixLabel, verification, source, languageBackupId: backup ? backup.id : null,
379
+ ...(languageGroupId ? { languageGroupId } : {}),
379
380
  };
380
381
  log.push(entry);
381
382
  await _writeLogAndSync(scanRoot, log);
@@ -517,13 +518,38 @@ async function _revertEntryInner(scanRoot, entry) {
517
518
  return entry;
518
519
  }
519
520
 
521
+ // A multi-file language fix is recorded as one entry per file sharing `languageGroupId`. They are reverted as a unit: every
522
+ // backup is read first, and if a restore fails the files already restored are put back, so a half-undone fix is never left.
523
+ async function _revertGroupInner(scanRoot, entries) {
524
+ const items = [];
525
+ for (const entry of entries) {
526
+ const bak = path.resolve(scanRoot, entry.backupPath);
527
+ if (!fs.existsSync(bak)) return { error: `backup missing: ${bak}` };
528
+ const abs = path.resolve(scanRoot, entry.file);
529
+ items.push({ entry, abs, original: await fsp.readFile(bak, 'utf8'), current: fs.existsSync(abs) ? await fsp.readFile(abs, 'utf8') : null });
530
+ }
531
+ const done = [];
532
+ for (const it of items) {
533
+ try { await _writeAtomicAndSync(it.abs, it.original); done.push(it); } catch (err) {
534
+ const failed = [];
535
+ for (const d of done.reverse()) { try { if (d.current !== null) await _writeAtomicAndSync(d.abs, d.current); } catch { failed.push(d.entry.file); } }
536
+ return { error: `group undo failed at ${it.entry.file} (${err.message}); ${failed.length ? `ROLLBACK INCOMPLETE for ${failed.join(', ')}` : 'no file was changed'}` };
537
+ }
538
+ }
539
+ const now = new Date().toISOString();
540
+ for (const it of items) { it.entry.reverted = true; it.entry.revertedAt = now; }
541
+ return items[items.length - 1].entry;
542
+ }
543
+ const _groupOf = (log, entry) => (entry.languageGroupId ? log.filter((e) => e.languageGroupId === entry.languageGroupId && !e.reverted) : null);
544
+
520
545
  // Revert the most recent un-reverted fix. Returns the entry or null.
521
546
  export async function undoLast(scanRoot) {
522
547
  return _withLogLock(scanRoot, async () => {
523
548
  const log = readLog(scanRoot);
524
549
  for (let i = log.length - 1; i >= 0; i--) {
525
550
  if (!log[i].reverted) {
526
- const result = await _revertEntryInner(scanRoot, log[i]);
551
+ const group = _groupOf(log, log[i]);
552
+ const result = group ? await _revertGroupInner(scanRoot, group) : await _revertEntryInner(scanRoot, log[i]);
527
553
  if (!result.error) await _writeLogAndSync(scanRoot, log);
528
554
  return result;
529
555
  }
@@ -550,7 +576,8 @@ export async function revertEntryById(scanRoot, entryId) {
550
576
  const entry = log.find(e => e.id === entryId);
551
577
  if (!entry) return { error: `no such history entry: ${entryId}` };
552
578
  if (entry.reverted) return entry;
553
- const result = await _revertEntryInner(scanRoot, entry);
579
+ const group = _groupOf(log, entry);
580
+ const result = group ? await _revertGroupInner(scanRoot, group) : await _revertEntryInner(scanRoot, entry);
554
581
  if (!result.error) await _writeLogAndSync(scanRoot, log);
555
582
  return result;
556
583
  });
@@ -542,7 +542,10 @@ export function normalizeFindings(scan){
542
542
  version: sc.version,
543
543
  cveAliases: sc.cveAliases || [],
544
544
  osvId: sc.osvId || null,
545
- advisory: sc.advisory || sc.description || '',
545
+ advisory: sc.advisory || sc.description || sc.summary || '',
546
+ // Hackage (and any matcher that decides from ranges): WHY the dependency matched. Without these a `possibly-affected` finding
547
+ // (only a declared range is known) reads exactly like an `affected` one (the resolved version is inside the range).
548
+ ...(sc.matchStatus ? { matchStatus: sc.matchStatus, matchReason: sc.matchReason || null, declaredRange: sc.declaredRange || null, resolution: sc.resolution || null, ghcComponent: sc.ghcComponent === true } : {}),
546
549
  fixedIn: sc.range || (Array.isArray(sc.fixedIn) && sc.fixedIn.length ? sc.fixedIn : null),
547
550
  ...(Number.isInteger(sc.line) && sc.line > 0 ? { declaringLine: sc.line, declaringFile: sc.file || null } : {}),
548
551
  // Feat-9: real-world risk signals