Implement Bun lockfile parser, enhance scanner output, and update dependencies#24
Implement Bun lockfile parser, enhance scanner output, and update dependencies#24
Conversation
…ng, and refactor scanner output to string rendering
…data and improve scanner path resolution
…egexp' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Miccy <code@miccy.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Miccy <code@miccy.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Miccy <code@miccy.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Miccy <code@miccy.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Miccy <code@miccy.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Miccy <code@miccy.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Miccy <code@miccy.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Miccy <code@miccy.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Miccy <code@miccy.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Miccy <code@miccy.dev>
Updated the search component layout to use CSS Grid, allowing the cancel button to have a flexible width based on its content. This accommodates different word lengths in localized translations. - Replaced absolute positioning with CSS Grid on .dialog-frame. - Used display: contents for intermediate search containers to allow nested elements to participate in the grid. - Removed hardcoded --sl-search-cancel-space and width calculations. - Added a Playwright test to verify layout flexibility.
Removed debug console.log statements from apps/docs/test-loader.ts. Since the file only contained these logs and their supporting boilerplate, and it was not referenced anywhere in the codebase, the entire file was deleted to improve code health.
STIX 2.1 bundle IDs should be unique and ideally non-predictable. Replacing Date.now() with crypto.randomUUID() ensures uniqueness and adheres to security best practices for identifier generation.
Removed the unused underscore from the _dryRun parameter and implemented conditional logging to respect the dryRun flag.
- Added validatePath utility to prevent null byte injection and empty paths - Applied validation at all scanner entry points: scan(), findLockfiles(), parseNpmLockfile(), parseYarnLockfile(), detectInjection() - Added security tests for path validation
BREAKING CHANGES: - sha256 field in file_artifacts now requires 64-char hex or null - pnpm parser separates resolved and integrity fields - IOC/remediation arrays reject empty strings Security: - SSRF protection in ingest.ts (private URL blocking, timeouts, size limits) - Path traversal validation in scanner (null byte, empty path) Fixed: - Bun/pnpm alias descriptor parsing (@npm: patterns) - Bun lockfile deduplication (prefer bun.lock over bun.lockb) - Per-entry threat catalog loading (one bad file no longer drops all) - Threat shape validation before indexing - readdirSync TOCTOU race in threat loader - toFindingSeverity default case - CLI writeFileSync error handling - npm parser: skip root/link entries in v3 lockfiles - Node ESM: replaced bare JSON imports with fs.readFileSync - Empty sha256 indicators replaced with null - Removed redundant parseNpmLock wrapper - Dynamic test discovery for threat catalog validation - Playwright config: pipe stdout/stderr for CI debugging
Dependencies updated: - ora: 9.3.0 → 9.4.0 - @astrojs/check: 0.9.8 → 0.9.9 - @astrojs/starlight: 0.38.3 → 0.38.4 - astro: 6.1.8 → 6.2.1 - marked: 18.0.2 → 18.0.3 - vite: 7.0.0 → 8.0.10 (major) - zod: 4.3.6 → 4.4.2 - @biomejs/biome: 2.4.12 → 2.4.14 - turbo: 2.9.6 → 2.9.8 - @types/bun: 1.3.12 → 1.3.13 (all workspaces) Renamed: - Replaced all 'dont-be-shy-hulud' references with 'wormsCTRL' across 37 files (docs, scripts, configs, AGENTS.md, SECURITY.md) - Fixed package.json repository URLs to match GitHub repo name
- Fixed curl/script URLs in installation.md and quickstart.md to use correct path packages/scripts/detect.sh (not scripts/detect.sh) - Expanded isPrivateUrl loopback check from exact 127.0.0.1 to full 127.0.0.0/8 range via regex - Added TODO for IPv6 private range coverage (fc00::/7, fe80::/10) Rejected CodeRabbit findings (5 of 8): - dont-be-shy-hulud rename suggestions: that is the OLD name, wormsCTRL is the current GitHub repo - worms-ctrl npm name: correct published name per package.json bin - event-stream sha256 null: original values were empty strings, null correctly represents unknown hashes per updated schema
…r robustness across parsers and threat validation
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Miccy <code@miccy.dev>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Miccy <code@miccy.dev>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 93 out of 101 changed files in this pull request and generated 11 comments.
Files not reviewed (3)
- tests/fixtures/axios-compromise/package-lock.json: Language not supported
- tests/fixtures/clean-baseline/package-lock.json: Language not supported
- tests/fixtures/shai-hulud-worm/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| parsedPackages.push({ | ||
| name, | ||
| version, | ||
| lockfileVersion: 0, | ||
| }) |
| if (!existsSync(WIKI_DIR)) { | ||
| console.error(`❌ Wiki directory not found: ${WIKI_DIR}`) | ||
| console.log(' Clone the wiki repo first:') | ||
| console.log(' git clone https://github.com/miccy/dont-be-shy-hulud.wiki.git') | ||
| console.log(' git clone https://github.com/miccy/wormsCTRL.wiki.git') | ||
| process.exit(1) |
| export function parseLockfiles(targetDir: string): LockfilePackage[] { | ||
| return [ | ||
| ...parseNpmLockfile(targetDir), | ||
| ...parseYarnLockfile(targetDir), | ||
| ...parsePnpmLockfile(targetDir), | ||
| ...parseBunLockfile(targetDir), | ||
| ] |
| export function formatSarif(result: ScanResult): SarifReport { | ||
| const rules = new Map<string, SarifRule>() | ||
|
|
||
| for (const finding of result.findings) { |
| { | ||
| "type": "bundle", | ||
| "id": "bundle--3d0c41df-9d58-45da-9b59-4d8727284f18", | ||
| "spec_version": "2.1", | ||
| "objects": [ | ||
| { |
| function isValidThreatShape(obj: unknown): obj is ThreatObject { | ||
| if (!obj || typeof obj !== 'object') return false | ||
| const candidate = obj as Record<string, unknown> | ||
| if (typeof candidate.id !== 'string') return false | ||
| if (!['CRITICAL', 'HIGH', 'MODERATE', 'LOW'].includes(candidate.severity as string)) return false | ||
| if (typeof candidate.attack_vector !== 'string') return false |
| function generateFooter(): string { | ||
| return `--- | ||
| 📖 [Documentation](https://hulud.dev) | 🐙 [GitHub](https://github.com/miccy/dont-be-shy-hulud) | 🪱 v1.5.1 | ||
| 📖 [Documentation](https://hulud.dev) | 🐙 [GitHub](https://github.com/miccy/wormsCTRL) | 🪱 v1.5.1 | ||
| ` |
| export function detect(targetDir: string): Finding[] { | ||
| const findings: Finding[] = [] | ||
| const lockPkgs = parseNpmLockfile(targetDir) | ||
| const lockPkgs = parseLockfiles(targetDir) | ||
| findings.push(...detectInjection(lockPkgs, targetDir)) |
| if (dryRun) { | ||
| console.log(`[safe-suspend] [DRY-RUN] Would freeze processes matching: ${sig}`) | ||
| } else { | ||
| console.log(`[safe-suspend] Freezing processes matching: ${sig}`) | ||
| // TODO/FIXME: implement the actual suspension using child_process (pgrep + kill -STOP) | ||
| throw new Error('suspension not implemented') | ||
| } |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Miccy <code@miccy.dev>
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (1)
packages/scanner/src/output/sarif.ts (1)
63-73: ⚡ Quick winPrefer exhaustive
Finding['type']mapping over string normalization fallback.Line 72–73 adds runtime normalization, but
Finding.typeis already a strict kebab-case union. UsingRecord<Finding['type'], string>gives compile-time coverage and avoids silently returningWCTRL/scan/unknownwhen a new finding type is added but not mapped.Suggested diff
function ruleId(finding: Finding): string { - const map: Record<string, string> = { + const map: Record<Finding['type'], string> = { injection: 'WCTRL/scan/injected-package', 'hash-mismatch': 'WCTRL/scan/hash-mismatch', doppelganger: 'WCTRL/scan/doppelganger', 'malicious-package': 'WCTRL/scan/malicious-package', 'suspicious-script': 'WCTRL/scan/suspicious-script', } - - // Normalize finding.type from camelCase to kebab-case if necessary - const normalizedType = finding.type.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase() - return map[normalizedType] ?? map[finding.type] ?? 'WCTRL/scan/unknown' + return map[finding.type] }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/scanner/src/output/sarif.ts` around lines 63 - 73, Replace the runtime normalization and fallback with an exhaustive compile-time mapping: change the map variable to have type Record<Finding['type'], string> (using the Finding type union) and remove normalizedType logic; perform a direct lookup using map[finding.type] so TypeScript will error if a new Finding type is added but not mapped (optionally, if you need a runtime guard, use an explicit exhaustive switch or a type assertion with a clear error message rather than silently returning 'WCTRL/scan/unknown').
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cs/README.md`:
- Around line 34-40: Update the README examples to clarify command behavior:
state that the scan subcommand's --threats flag only logs the number of loaded
threat objects during scan (i.e., a quick count/diagnostic) and does not output
the full catalog, and explicitly document that the full threat catalog is
produced by the separate top-level command npx worms-ctrl threats; adjust the
example block under the optional formats/examples so one line shows npx
worms-ctrl scan . --threats (with a parenthetical like “prints loaded threat
count only”) and another line shows npx worms-ctrl threats (with a parenthetical
like “prints full threat catalog”) so users can see the difference between the
--threats flag and the threats command.
In `@package.json`:
- Around line 65-67: The package manifest currently pins Vite 8 via the
"overrides" entry ("vite": "8.0.10") but the root "engines" field still allows
Node >=18, which is below Vite 8's minimum; either update the root "engines"
Node constraint to ">=20.19" (or ">=22.12" if you prefer to require Node 22) to
match Vite 8, or remove/move the "overrides" entry into the specific workspace
package.json that actually depends on Vite 8 so the root manifest doesn't force
a higher Node requirement for all contributors.
In `@packages/engine/src/types.ts`:
- Around line 3-13: The type unions (ThreatEcosystem, ThreatSeverity,
ThreatStatus) currently include values (e.g., "maven", "nuget", "gem", "LOW",
"UNDER_REVIEW") that packages/engine/src/validate.ts rejects for ThreatObject;
fix by aligning types with runtime validation: either narrow
ThreatEcosystem/ThreatSeverity/ThreatStatus to only the values accepted by
validate.ts or change validate.ts to accept the broader set; implement the
preferred approach by splitting profile-level unions from object-level unions
(create separate types like ThreatProfileEcosystem vs ThreatObjectEcosystem or
adjust ThreatObject's field types) and update references to ThreatObject,
ThreatEcosystem, ThreatSeverity, and ThreatStatus accordingly so compile-time
types match runtime validation.
In `@packages/ioc/index.js`:
- Around line 55-57: Parsed JSON is currently pushed directly into threats which
allows arrays/primitives or objects missing required fields; replace the direct
push with parsing into a local variable (e.g., const parsed =
JSON.parse(content)), then validate that parsed is a non-null plain object
(typeof === 'object' && parsed !== null && !Array.isArray(parsed>) and contains
required properties 'id' and 'status' before calling threats.push(parsed); if
validation fails, skip pushing and emit a warning/error log referencing
THREATS_DIR and entry so invalid files are ignored.
In `@packages/remediation/src/scripts/safe-suspend.ts`:
- Around line 11-22: The non-dry-run path in safeSuspend is broken because it
throws inside the loop on first signature, so running safeSuspend() (dryRun
default false) never completes; update safeSuspend to either implement actual
suspension logic (use child_process to pgrep by signature and send SIGSTOP via
kill for each PID) or, until implementation exists, fail fast before the loop
when dryRun === false by throwing a clear "non-dry-run unsupported" error and
avoid emitting "Freezing…" logs; refer to the safeSuspend function and
MALICIOUS_SIGNATURES constant when making this change so logs and control flow
are corrected.
In `@packages/scanner/src/parsers/bun.ts`:
- Around line 9-15: The fallback resolution builds paths incorrectly when given
a file path (e.g., "/repo/bun.lock" or "/repo/bun.lockb") causing joins like
"/repo/bun.lock/bun.lockb" instead of checking the sibling file; update
resolveBunLockTextPath and the corresponding binary resolver (e.g.,
resolveBunLockBinaryPath) to: if basename(targetOrPath) matches the lock
filename check existsSync(targetOrPath) as now, else treat it as a directory and
check join(targetOrPath, '<name>'); additionally, if the input is a file path
for one lock variant, also check the sibling in the same directory by using
dirname(targetOrPath) + join(dirname, '<otherLockName>') so "/repo/bun.lock"
will correctly discover "/repo/bun.lockb"; use basename, dirname, join and
existsSync to implement these checks.
In `@packages/scanner/src/parsers/js-yaml.d.ts`:
- Around line 10-13: Update the ambient module declaration to match js-yaml v4:
remove the deprecated/incorrect safeLoad (and safeLoadAll if present) exports
and delete the deprecation comment on load so that export function load(...) is
the safe default; locate the declarations for load and safeLoad in the module
(symbols "load" and "safeLoad") and remove the safeLoad export and its mention,
leaving load(...) typed normally without the deprecation comment.
In `@packages/scanner/src/parsers/pnpm.ts`:
- Around line 123-136: The parser currently reads dependency edges from
entry.dependencies inside the packages loop (in the block using
parsePnpmPackageKey and pushing into parsedPackages), but for pnpm v9 dependency
relations live in lock.snapshots; update the logic that sets the requires field
so it prefers snapshot dependencies when available: if lock.snapshots exists,
look up the corresponding snapshot by the same package identifier (use the
parsed key or package id derived in parsePnpmPackageKey) and use
snapshot.dependencies, falling back to entry.dependencies when no snapshot is
found; ensure this change is applied where requires is assigned in the
parsedPackages.push so transitive dependency data is preserved for v9+
lockfiles.
- Around line 42-84: The npm alias handling for '@npm:' (the logic using
npmAliasIdx, aliasName and realPart) must be run before the scoped branch (if
(normalized.startsWith('@')) ) and the slash branch (if
(normalized.includes('/')) ) because entries like '@scope/alias@npm:pkg@1.2.3'
are currently parsed by those branches and produce incorrect name/version; move
the block that computes npmAliasIdx, aliasName, realPart and version to just
after you obtain normalized (i.e., before any checks for startsWith('@') or
includes('/')) so that when normalized.indexOf('@npm:') > 0 you return the alias
result immediately and skip the scoped/slash parsing. Ensure you keep the same
name/version extraction logic (realPart.lastIndexOf('@') and split('_')[0]) when
relocating the code.
In `@README.md`:
- Around line 34-40: The README mistakenly suggests `--threats` is a catalog
command; update documentation to show the separate catalog subcommand and
clarify the flag's behavior: add an explicit example line with `npx worms-ctrl
threats` for catalog output, and change the existing example or note to state
that `npx worms-ctrl scan ... --threats` only reports loaded threat count during
a scan (as implemented in packages/scanner/src/cli.ts, where the scan command
handles the --threats flag). Ensure the README mentions both usages (the
`threats` subcommand for catalog listing and the `--threats` scan flag for
count) so readers aren’t misled.
---
Nitpick comments:
In `@packages/scanner/src/output/sarif.ts`:
- Around line 63-73: Replace the runtime normalization and fallback with an
exhaustive compile-time mapping: change the map variable to have type
Record<Finding['type'], string> (using the Finding type union) and remove
normalizedType logic; perform a direct lookup using map[finding.type] so
TypeScript will error if a new Finding type is added but not mapped (optionally,
if you need a runtime guard, use an explicit exhaustive switch or a type
assertion with a clear error message rather than silently returning
'WCTRL/scan/unknown').
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 3ca1bd55-0965-48ac-bf37-93ae16fa6d6b
📒 Files selected for processing (58)
.gitignoreAGENTS.mdCHANGELOG.mdREADME.mdSECURITY.mdapps/docs/playwright.config.tsapps/docs/tests/docs.e2e.tsapps/docs/tests/verify-search.spec.tscs/README.mdexamples/axios-compromise.sarifpackage.jsonpackages/configs/bunfig-secure.tomlpackages/configs/pnpm-workspace-secure.yamlpackages/configs/renovate-defense.jsonpackages/docs-content/cs/getting-started/installation.mdpackages/docs-content/cs/index.mdxpackages/docs-content/cs/meta/AGENTS.mdpackages/docs-content/cs/meta/README.mdpackages/docs-content/cs/meta/SECURITY.mdpackages/docs-content/cs/reference/cli.mdpackages/docs-content/cs/reference/ioc-database.mdpackages/docs-content/getting-started/installation.mdpackages/docs-content/getting-started/quickstart.mdpackages/docs-content/reference/cli.mdpackages/docs-content/reference/ioc-database.mdpackages/engine/src/__tests__/threats.validate.test.tspackages/engine/src/extractor/ioc.test.tspackages/engine/src/extractor/ioc.tspackages/engine/src/ingest.tspackages/engine/src/prompt.tspackages/engine/src/types.tspackages/engine/src/validate.tspackages/engine/tests/osv.test.tspackages/ioc/index.jspackages/ioc/threats/axios-2026.jsonpackages/ioc/threats/ctx-2022.jsonpackages/ioc/threats/event-stream-2018.jsonpackages/ioc/threats/shai-hulud-2025.jsonpackages/remediation/src/scripts/safe-suspend.tspackages/scanner/src/detectors/injection.tspackages/scanner/src/output/sarif.tspackages/scanner/src/parsers/bun.tspackages/scanner/src/parsers/index.tspackages/scanner/src/parsers/js-yaml.d.tspackages/scanner/src/parsers/pnpm.tspackages/scanner/src/parsers/requirements.tspackages/scanner/src/scan.tspackages/scanner/src/threats.tspackages/scanner/src/types.tspackages/scanner/src/utils.tspackages/scanner/tests/security.test.tspackages/scripts/check-github-repos.shpackages/scripts/detect.shpackages/scripts/full-audit.shpackages/scripts/harden-npm.shpackages/scripts/quick-audit.shpackages/scripts/release.shsocket.yml
✅ Files skipped from review due to trivial changes (27)
- packages/configs/pnpm-workspace-secure.yaml
- packages/configs/bunfig-secure.toml
- packages/engine/src/extractor/ioc.test.ts
- packages/docs-content/reference/cli.md
- packages/scripts/release.sh
- packages/ioc/threats/ctx-2022.json
- packages/ioc/threats/event-stream-2018.json
- examples/axios-compromise.sarif
- packages/docs-content/reference/ioc-database.md
- packages/docs-content/cs/reference/ioc-database.md
- packages/engine/src/prompt.ts
- packages/engine/src/validate.ts
- packages/scripts/full-audit.sh
- packages/docs-content/cs/meta/AGENTS.md
- packages/ioc/threats/shai-hulud-2025.json
- packages/scripts/quick-audit.sh
- packages/docs-content/cs/getting-started/installation.md
- packages/scanner/src/types.ts
- packages/scripts/check-github-repos.sh
- AGENTS.md
- packages/scripts/harden-npm.sh
- packages/engine/tests/osv.test.ts
- packages/docs-content/getting-started/quickstart.md
- packages/engine/src/ingest.ts
- packages/docs-content/cs/meta/README.md
- SECURITY.md
- packages/scripts/detect.sh
🚧 Files skipped from review as they are similar to previous changes (13)
- packages/configs/renovate-defense.json
- packages/engine/src/extractor/ioc.ts
- apps/docs/tests/verify-search.spec.ts
- packages/ioc/threats/axios-2026.json
- packages/docs-content/getting-started/installation.md
- packages/scanner/tests/security.test.ts
- packages/scanner/src/parsers/index.ts
- packages/docs-content/cs/reference/cli.md
- packages/scanner/src/detectors/injection.ts
- packages/scanner/src/scan.ts
- CHANGELOG.md
- packages/scanner/src/parsers/requirements.ts
- packages/scanner/src/threats.ts
| Volitelné formáty: | ||
|
|
||
| ```bash | ||
| npx worms-ctrl scan . --format json | ||
| npx worms-ctrl scan . --format sarif --output wormsctrl.sarif | ||
| npx worms-ctrl scan . --threats | ||
| ``` |
There was a problem hiding this comment.
Clarify scan --threats vs threats command behavior.
Line 39 currently reads like a catalog command, but --threats in packages/scanner/src/cli.ts only logs the loaded threat-object count during scan; full catalog output is npx worms-ctrl threats. Please document both explicitly to avoid UX confusion.
Suggested doc tweak
npx worms-ctrl scan . --format json
npx worms-ctrl scan . --format sarif --output wormsctrl.sarif
-npx worms-ctrl scan . --threats
+npx worms-ctrl scan . --threats # vypíše počet načtených threat objektů
+npx worms-ctrl threats # vypíše celý katalog threatů🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cs/README.md` around lines 34 - 40, Update the README examples to clarify
command behavior: state that the scan subcommand's --threats flag only logs the
number of loaded threat objects during scan (i.e., a quick count/diagnostic) and
does not output the full catalog, and explicitly document that the full threat
catalog is produced by the separate top-level command npx worms-ctrl threats;
adjust the example block under the optional formats/examples so one line shows
npx worms-ctrl scan . --threats (with a parenthetical like “prints loaded threat
count only”) and another line shows npx worms-ctrl threats (with a parenthetical
like “prints full threat catalog”) so users can see the difference between the
--threats flag and the threats command.
| export async function safeSuspend(dryRun = false): Promise<void> { | ||
| console.log('[safe-suspend] Scanning for malicious processes...') | ||
|
|
||
| for (const sig of MALICIOUS_SIGNATURES) { | ||
| // Placeholder: actual pgrep implementation | ||
| console.log(`[safe-suspend] Would freeze processes matching: ${sig}`) | ||
| if (dryRun) { | ||
| console.log(`[safe-suspend] [DRY-RUN] Would freeze processes matching: ${sig}`) | ||
| } else { | ||
| console.log(`[safe-suspend] Freezing processes matching: ${sig}`) | ||
| // TODO/FIXME: implement the actual suspension using child_process (pgrep + kill -STOP) | ||
| throw new Error('suspension not implemented') | ||
| } |
There was a problem hiding this comment.
Non-dry-run path is always broken right now
Line 11 defaults to dryRun = false, but Lines 19–21 always throw on the first loop iteration, so default execution can never complete remediation. This is a functional gap in the main path; either implement SIGSTOP here or fail fast once before the loop with an explicit “non-dry-run unsupported” error (and avoid “Freezing…” logs that imply action occurred).
Suggested minimal safeguard until SIGSTOP implementation lands
export async function safeSuspend(dryRun = false): Promise<void> {
console.log('[safe-suspend] Scanning for malicious processes...')
+ if (!dryRun) {
+ throw new Error('[safe-suspend] non-dry-run mode is not implemented yet')
+ }
for (const sig of MALICIOUS_SIGNATURES) {
- // Placeholder: actual pgrep implementation
- if (dryRun) {
- console.log(`[safe-suspend] [DRY-RUN] Would freeze processes matching: ${sig}`)
- } else {
- console.log(`[safe-suspend] Freezing processes matching: ${sig}`)
- // TODO/FIXME: implement the actual suspension using child_process (pgrep + kill -STOP)
- throw new Error('suspension not implemented')
- }
+ // Placeholder: actual pgrep + kill -STOP implementation
+ console.log(`[safe-suspend] [DRY-RUN] Would freeze processes matching: ${sig}`)
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function safeSuspend(dryRun = false): Promise<void> { | |
| console.log('[safe-suspend] Scanning for malicious processes...') | |
| for (const sig of MALICIOUS_SIGNATURES) { | |
| // Placeholder: actual pgrep implementation | |
| console.log(`[safe-suspend] Would freeze processes matching: ${sig}`) | |
| if (dryRun) { | |
| console.log(`[safe-suspend] [DRY-RUN] Would freeze processes matching: ${sig}`) | |
| } else { | |
| console.log(`[safe-suspend] Freezing processes matching: ${sig}`) | |
| // TODO/FIXME: implement the actual suspension using child_process (pgrep + kill -STOP) | |
| throw new Error('suspension not implemented') | |
| } | |
| export async function safeSuspend(dryRun = false): Promise<void> { | |
| console.log('[safe-suspend] Scanning for malicious processes...') | |
| if (!dryRun) { | |
| throw new Error('[safe-suspend] non-dry-run mode is not implemented yet') | |
| } | |
| for (const sig of MALICIOUS_SIGNATURES) { | |
| // Placeholder: actual pgrep + kill -STOP implementation | |
| console.log(`[safe-suspend] [DRY-RUN] Would freeze processes matching: ${sig}`) | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/remediation/src/scripts/safe-suspend.ts` around lines 11 - 22, The
non-dry-run path in safeSuspend is broken because it throws inside the loop on
first signature, so running safeSuspend() (dryRun default false) never
completes; update safeSuspend to either implement actual suspension logic (use
child_process to pgrep by signature and send SIGSTOP via kill for each PID) or,
until implementation exists, fail fast before the loop when dryRun === false by
throwing a clear "non-dry-run unsupported" error and avoid emitting "Freezing…"
logs; refer to the safeSuspend function and MALICIOUS_SIGNATURES constant when
making this change so logs and control flow are corrected.
| if (normalized.startsWith('@')) { | ||
| const scopeSeparator = normalized.indexOf('/', 1) | ||
| if (scopeSeparator === -1) { | ||
| return null | ||
| } | ||
|
|
||
| const versionAtSeparator = normalized.indexOf('@', scopeSeparator + 1) | ||
| if (versionAtSeparator !== -1) { | ||
| return { | ||
| name: normalized.slice(0, versionAtSeparator), | ||
| version: normalized.slice(versionAtSeparator + 1).split('_')[0] ?? '', | ||
| } | ||
| } | ||
|
|
||
| const versionSlashSeparator = normalized.indexOf('/', scopeSeparator + 1) | ||
| if (versionSlashSeparator !== -1) { | ||
| return { | ||
| name: normalized.slice(0, versionSlashSeparator), | ||
| version: normalized.slice(versionSlashSeparator + 1).split('_')[0] ?? '', | ||
| } | ||
| } | ||
|
|
||
| return null | ||
| } | ||
|
|
||
| if (normalized.includes('/')) { | ||
| const versionSeparator = normalized.indexOf('/') | ||
| return { | ||
| name: normalized.slice(0, versionSeparator), | ||
| version: normalized.slice(versionSeparator + 1).split('_')[0] ?? '', | ||
| } | ||
| } | ||
|
|
||
| // Handle npm alias keys: alias@npm:real@version | ||
| const npmAliasIdx = normalized.indexOf('@npm:') | ||
| if (npmAliasIdx > 0) { | ||
| const aliasName = normalized.slice(0, npmAliasIdx) | ||
| const realPart = normalized.slice(npmAliasIdx + 5) | ||
| const realVersionSep = realPart.lastIndexOf('@') | ||
| const version = | ||
| realVersionSep > 0 ? (realPart.slice(realVersionSep + 1).split('_')[0] ?? '') : '' | ||
| return { name: aliasName, version } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "pnpm.ts" | grep -E "scanner|parsers"Repository: miccy/worms-ctrl
Length of output: 99
🏁 Script executed:
cat -n ./packages/scanner/src/parsers/pnpm.tsRepository: miccy/worms-ctrl
Length of output: 5049
🏁 Script executed:
find . -type f -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "pnpm\|parsePnpmPackageKey" 2>/dev/nullRepository: miccy/worms-ctrl
Length of output: 109
🏁 Script executed:
cat -n ./packages/scanner/src/__tests__/parsers.test.tsRepository: miccy/worms-ctrl
Length of output: 3982
Move @npm: alias handling before scoped and slash branches to prevent corrupted name/version parsing.
Keys like @scope/alias@npm:pkg@1.2.3 or alias@npm:@scope/pkg@1.2.3 are currently misinterpreted by the scoped/slash parsers. For example, @scope/alias@npm:pkg@1.2.3 gets parsed as name: @scope/alias and version: npm:pkg@1.2.3, which breaks threat matching for aliased dependencies.
Suggested fix
function parsePnpmPackageKey(key: string): { name: string; version: string } | null {
const normalized = key.replace(/^\/+/, '').replace(/\([^)]*\)/g, '')
if (!normalized) {
return null
}
+
+ // Handle npm alias keys before generic scoped/unscoped parsing.
+ const npmAliasIdx = normalized.indexOf('@npm:')
+ if (npmAliasIdx > 0) {
+ const aliasName = normalized.slice(0, npmAliasIdx)
+ const realPart = normalized.slice(npmAliasIdx + 5)
+ const realVersionSep = realPart.lastIndexOf('@')
+ const version =
+ realVersionSep > 0 ? (realPart.slice(realVersionSep + 1).split('_')[0] ?? '') : ''
+ return { name: aliasName, version }
+ }
if (normalized.startsWith('@')) {
const scopeSeparator = normalized.indexOf('/', 1)
if (scopeSeparator === -1) {
return null
@@
- // Handle npm alias keys: alias@npm:real@version
- const npmAliasIdx = normalized.indexOf('@npm:')
- if (npmAliasIdx > 0) {
- const aliasName = normalized.slice(0, npmAliasIdx)
- const realPart = normalized.slice(npmAliasIdx + 5)
- const realVersionSep = realPart.lastIndexOf('@')
- const version =
- realVersionSep > 0 ? (realPart.slice(realVersionSep + 1).split('_')[0] ?? '') : ''
- return { name: aliasName, version }
- }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/scanner/src/parsers/pnpm.ts` around lines 42 - 84, The npm alias
handling for '@npm:' (the logic using npmAliasIdx, aliasName and realPart) must
be run before the scoped branch (if (normalized.startsWith('@')) ) and the slash
branch (if (normalized.includes('/')) ) because entries like
'@scope/alias@npm:pkg@1.2.3' are currently parsed by those branches and produce
incorrect name/version; move the block that computes npmAliasIdx, aliasName,
realPart and version to just after you obtain normalized (i.e., before any
checks for startsWith('@') or includes('/')) so that when
normalized.indexOf('@npm:') > 0 you return the alias result immediately and skip
the scoped/slash parsing. Ensure you keep the same name/version extraction logic
(realPart.lastIndexOf('@') and split('_')[0]) when relocating the code.
| for (const [key, entry] of Object.entries(packages)) { | ||
| const parsedKey = parsePnpmPackageKey(key) | ||
| if (!parsedKey) { | ||
| continue | ||
| } | ||
|
|
||
| parsedPackages.push({ | ||
| ...parsedKey, | ||
| resolved: entry.resolution?.tarball, | ||
| integrity: entry.resolution?.integrity, | ||
| engines: entry.engines, | ||
| requires: entry.dependencies, | ||
| lockfileVersion: lock.lockfileVersion ?? 'unknown', | ||
| }) |
There was a problem hiding this comment.
Pull dependency edges from snapshots for v9 lockfiles.
pnpm v9 moved dependency relations out of packages and into snapshots. Reading entry.dependencies here means requires will be empty for those entries, so transitive detections and parser parity regress on modern lockfiles. This needs a snapshot join before returning ParsedPackage[]. (github.com)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/scanner/src/parsers/pnpm.ts` around lines 123 - 136, The parser
currently reads dependency edges from entry.dependencies inside the packages
loop (in the block using parsePnpmPackageKey and pushing into parsedPackages),
but for pnpm v9 dependency relations live in lock.snapshots; update the logic
that sets the requires field so it prefers snapshot dependencies when available:
if lock.snapshots exists, look up the corresponding snapshot by the same package
identifier (use the parsed key or package id derived in parsePnpmPackageKey) and
use snapshot.dependencies, falling back to entry.dependencies when no snapshot
is found; ensure this change is applied where requires is assigned in the
parsedPackages.push so transitive dependency data is preserved for v9+
lockfiles.
| Optional formats: | ||
|
|
||
| # View the Knowledge Base of tracked threats | ||
| npx worms-ctrl threats | ||
| ```bash | ||
| npx worms-ctrl scan . --format json | ||
| npx worms-ctrl scan . --format sarif --output wormsctrl.sarif | ||
| npx worms-ctrl scan . --threats | ||
| ``` |
There was a problem hiding this comment.
Document the catalog command separately from the scan flag.
Line 39 implies --threats is the catalog view, but in packages/scanner/src/cli.ts it only reports loaded threat count during scan. Add npx worms-ctrl threats explicitly for catalog output.
Suggested doc tweak
npx worms-ctrl scan . --format json
npx worms-ctrl scan . --format sarif --output wormsctrl.sarif
-npx worms-ctrl scan . --threats
+npx worms-ctrl scan . --threats # prints loaded threat object count
+npx worms-ctrl threats # prints full threat catalog📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Optional formats: | |
| # View the Knowledge Base of tracked threats | |
| npx worms-ctrl threats | |
| ```bash | |
| npx worms-ctrl scan . --format json | |
| npx worms-ctrl scan . --format sarif --output wormsctrl.sarif | |
| npx worms-ctrl scan . --threats | |
| ``` | |
| Optional formats: | |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@README.md` around lines 34 - 40, The README mistakenly suggests `--threats`
is a catalog command; update documentation to show the separate catalog
subcommand and clarify the flag's behavior: add an explicit example line with
`npx worms-ctrl threats` for catalog output, and change the existing example or
note to state that `npx worms-ctrl scan ... --threats` only reports loaded
threat count during a scan (as implemented in packages/scanner/src/cli.ts, where
the scan command handles the --threats flag). Ensure the README mentions both
usages (the `threats` subcommand for catalog listing and the `--threats` scan
flag for count) so readers aren’t misled.
🧹 Resolve PR #24 code review comments & bump to v2.0.0
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
packages/engine/tests/osv.test.ts (2)
20-27: ⚡ Quick winConsider asserting
modified/publishedpassthrough fields too.Those fields are part of the converter output, so one explicit assertion helps catch accidental omissions in future refactors.
Suggested assertion snippet
test('converts basic OSV record correctly', () => { const result = osvToThreatProfile(baseOsv) as unknown as Record<string, unknown> expect(result.id).toBe(baseOsv.id) expect(result.name).toBe('test-package') expect(result.ecosystem).toBe('npm') expect(result.severity).toBe('LOW') // Default score 0 -> LOW expect(result.status).toBe('UNDER_REVIEW') + expect(result.modified).toBe(baseOsv.modified) + expect(result.published).toBe(baseOsv.published) })Also applies to: 101-123
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/engine/tests/osv.test.ts` around lines 20 - 27, The test "converts basic OSV record correctly" currently asserts id, name, ecosystem, severity, and status but omits passthrough date fields; update the test that calls osvToThreatProfile(baseOsv) to also assert that the output includes the original modified and published values from baseOsv (e.g. expect(result.modified).toBe(baseOsv.modified) and expect(result.published).toBe(baseOsv.published)), so the converter's passthrough behavior for modified/published is explicitly verified.
29-58: ⚡ Quick winAdd one malformed
CVSS_V3score test case.Since OSV data is external, a non-numeric
score(for example'N/A') is realistic; adding this test will lock in expected fallback behavior and prevent future parser regressions.Suggested test addition
test('maps CVSS_V3 scores to severity correctly', () => { const testCases = [ { score: '9.5', expected: 'CRITICAL' }, { score: '9.0', expected: 'CRITICAL' }, { score: '8.9', expected: 'HIGH' }, { score: '7.0', expected: 'HIGH' }, { score: '6.9', expected: 'MEDIUM' }, { score: '4.0', expected: 'MEDIUM' }, { score: '3.9', expected: 'LOW' }, { score: '0.0', expected: 'LOW' }, + { score: 'N/A', expected: 'LOW' }, ]🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/engine/tests/osv.test.ts` around lines 29 - 58, Add a new test case in packages/engine/tests/osv.test.ts that supplies a malformed CVSS_V3 score (e.g., severity: [{ type: 'CVSS_V3', score: 'N/A' }]) and asserts osvToThreatProfile(osv).severity falls back to 'LOW'; update the existing "maps CVSS_V3 scores..." loop or add a separate small test to include this non-numeric score to lock in the parser's fallback behavior and reference osvToThreatProfile when locating the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/scanner/src/detectors/injection.ts`:
- Around line 21-42: loadPackageJsonDeps currently swallows parse/read errors
and returns null which disables the downstream gate (declared && pkg.version &&
!declared.has(pkg.name)); update the catch in loadPackageJsonDeps to log a
warning including the targetDir and the caught error (so operators can see
manifest read failures) and return an empty Set<string> (not null) so detection
still runs against the lockfile; ensure the log message includes context
(function name loadPackageJsonDeps and targetDir) and the error details.
- Around line 75-84: In the phantomMatches loop (where findings.push is called)
don't hardcode severity to 'critical'; use the same pattern as the
malicious-package block by deriving severity via
toFindingSeverity(match.threat.severity) (or, if you intentionally want to
escalate all phantom dependencies, add a one-line comment explaining that
decision next to the findings.push to make the intent explicit). Update the
severity assignment in the phantom dependency finding to reference match.threat
and toFindingSeverity so phantom IOC reports align with the threat catalog.
---
Nitpick comments:
In `@packages/engine/tests/osv.test.ts`:
- Around line 20-27: The test "converts basic OSV record correctly" currently
asserts id, name, ecosystem, severity, and status but omits passthrough date
fields; update the test that calls osvToThreatProfile(baseOsv) to also assert
that the output includes the original modified and published values from baseOsv
(e.g. expect(result.modified).toBe(baseOsv.modified) and
expect(result.published).toBe(baseOsv.published)), so the converter's
passthrough behavior for modified/published is explicitly verified.
- Around line 29-58: Add a new test case in packages/engine/tests/osv.test.ts
that supplies a malformed CVSS_V3 score (e.g., severity: [{ type: 'CVSS_V3',
score: 'N/A' }]) and asserts osvToThreatProfile(osv).severity falls back to
'LOW'; update the existing "maps CVSS_V3 scores..." loop or add a separate small
test to include this non-numeric score to lock in the parser's fallback behavior
and reference osvToThreatProfile when locating the test.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2852e907-94f0-4d88-8ee2-e5930a1f15bd
📒 Files selected for processing (5)
apps/docs/playwright.config.tspackages/engine/tests/osv.test.tspackages/scanner/src/detectors/injection.tspackages/scanner/src/parsers/js-yaml.d.tspackages/wiki-sync/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/wiki-sync/src/index.ts
- packages/scanner/src/parsers/js-yaml.d.ts
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 93 out of 101 changed files in this pull request and generated 10 comments.
Files not reviewed (3)
- tests/fixtures/axios-compromise/package-lock.json: Language not supported
- tests/fixtures/clean-baseline/package-lock.json: Language not supported
- tests/fixtures/shai-hulud-worm/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| parsedPackages.push({ | ||
| name, | ||
| version, | ||
| lockfileVersion: 0, | ||
| }) |
| if (normalized.startsWith('@')) { | ||
| const scopeSeparator = normalized.indexOf('/', 1) | ||
| if (scopeSeparator === -1) { | ||
| return null | ||
| } | ||
|
|
||
| const versionAtSeparator = normalized.indexOf('@', scopeSeparator + 1) | ||
| if (versionAtSeparator !== -1) { | ||
| return { | ||
| name: normalized.slice(0, versionAtSeparator), | ||
| version: normalized.slice(versionAtSeparator + 1).split('_')[0] ?? '', | ||
| } | ||
| } |
| { | ||
| "type": "bundle", | ||
| "id": "bundle--3d0c41df-9d58-45da-9b59-4d8727284f18", | ||
| "spec_version": "2.1", | ||
| "objects": [ | ||
| { |
| @@ -201,7 +201,7 @@ async function syncToWiki() { | |||
| if (!existsSync(WIKI_DIR)) { | |||
| console.error(`❌ Wiki directory not found: ${WIKI_DIR}`) | |||
| console.log(' Clone the wiki repo first:') | |||
| console.log(' git clone https://github.com/miccy/dont-be-shy-hulud.wiki.git') | |||
| console.log(' git clone https://github.com/miccy/wormsCTRL.wiki.git') | |||
| process.exit(1) | |||
| export function parseLockfiles(targetDir: string): LockfilePackage[] { | ||
| return [ | ||
| ...parseNpmLockfile(targetDir), | ||
| ...parseYarnLockfile(targetDir), | ||
| ...parsePnpmLockfile(targetDir), | ||
| ...parseBunLockfile(targetDir), | ||
| ] |
| - indicators_of_compromise must be an object with exactly these keys: | ||
| - package_names: array of strings | ||
| - malicious_versions: array of objects with { package: string, version: string, reason?: string } | ||
| - network_iocs: array of objects with { type: "ip" | "domain" | "url", value: string } | ||
| - file_artifacts: array of objects with { filename: string, sha256: string, path?: string } | ||
| - remediation must contain immediate and long_term arrays |
| function isValidThreatShape(obj: unknown): obj is ThreatObject { | ||
| if (!obj || typeof obj !== 'object') return false | ||
| const candidate = obj as Record<string, unknown> | ||
| if (typeof candidate.id !== 'string') return false | ||
| if (!['CRITICAL', 'HIGH', 'MODERATE', 'LOW'].includes(candidate.severity as string)) return false | ||
| if (typeof candidate.attack_vector !== 'string') return false | ||
| const ioc = candidate.indicators_of_compromise | ||
| if (!ioc || typeof ioc !== 'object') return false | ||
| const iocObj = ioc as Record<string, unknown> | ||
| if (!Array.isArray(iocObj.malicious_versions)) return false | ||
| return iocObj.malicious_versions.every((v: unknown) => { | ||
| if (!v || typeof v !== 'object') return false | ||
| const vObj = v as Record<string, unknown> | ||
| return typeof vObj.package === 'string' && typeof vObj.version === 'string' | ||
| }) | ||
| } |
| ### Added | ||
| - **Path validation** — Added `validatePath()` utility to scanner to prevent path traversal and reject null bytes/empty paths. | ||
| - **Threat database** — Added `xz-utils-2024.json` threat profile to `packages/ioc/threats`. |
| expect(newBox!.width).toBeGreaterThan(originalBox?.width || 0); | ||
|
|
||
| await page.screenshot({ path: 'search-modal-mobile-long-cancel.png' }); | ||
| }); |
| const cancelButton = page.locator('button[data-close-modal]'); | ||
| await expect(cancelButton).toBeHidden(); // Hidden on desktop in original code (md:sl-hidden) | ||
|
|
||
| await page.screenshot({ path: 'search-modal-desktop.png' }); | ||
| }); |
Replaces hardcoded fixed file paths for screenshots with testInfo.outputPath() in verify-search.spec.ts to prevent collisions during parallel test execution.
…4820 Signed-off-by: Miccy <support@miccy.dev>
🧹 Resolve PR #24 code review comments & bump to v2.0.0
…90430341770954550
…0430341770954550 🧪 fix: use testInfo.outputPath for playwright screenshots
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 93 out of 101 changed files in this pull request and generated 6 comments.
Files not reviewed (3)
- tests/fixtures/axios-compromise/package-lock.json: Language not supported
- tests/fixtures/clean-baseline/package-lock.json: Language not supported
- tests/fixtures/shai-hulud-worm/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (typeof candidate.id !== 'string') return false | ||
| if (!['CRITICAL', 'HIGH', 'MODERATE', 'LOW'].includes(candidate.severity as string)) return false | ||
| if (typeof candidate.attack_vector !== 'string') return false |
| parsedPackages.push({ | ||
| name, | ||
| version, | ||
| lockfileVersion: 0, | ||
| }) |
| { | ||
| "type": "bundle", | ||
| "id": "bundle--3d0c41df-9d58-45da-9b59-4d8727284f18", | ||
| "spec_version": "2.1", | ||
| "objects": [ |
| const CONTENT_DIR = join(import.meta.dirname, '../../docs-content/en') | ||
| const WIKI_DIR = join(import.meta.dirname, '../../../../dont-be-shy-hulud.wiki') | ||
| const WIKI_DIR = join(import.meta.dirname, '../../../../wormsCTRL.wiki') | ||
|
|
| - indicators_of_compromise must be an object with exactly these keys: | ||
| - package_names: array of strings | ||
| - malicious_versions: array of objects with { package: string, version: string, reason?: string } | ||
| - network_iocs: array of objects with { type: "ip" | "domain" | "url", value: string } | ||
| - file_artifacts: array of objects with { filename: string, sha256: string, path?: string } | ||
| - remediation must contain immediate and long_term arrays |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Miccy <code@miccy.dev>
|
@copilot implement all suggestion in one PR include suggestions from CodeRabbit. |
This pull request introduces a major update across the codebase, focused on the 2.0.0 release of the project (now called
wormsCTRL). The changes include a comprehensive rewrite of documentation, a new changelog, a new CLI packaging and build process, expanded threat catalog and scanning capabilities, and numerous bug fixes and security improvements. The update also introduces AI-assisted threat ingestion and validation, improved test coverage, and several developer experience enhancements.Key highlights:
Breaking Changes & Threat Schema Improvements
sha256fields must be valid 64-character hex ornull, and IOC/remediation arrays reject empty strings. Parser outputs for pnpm now separateresolvedandintegrityfields.1.5.2to2.0.0, with full details and migration notes in the newCHANGELOG.md.CLI Packaging & Build Enhancements
apps/cli/build.mjs) to bundle the CLI and threat catalog for standalone distribution, and updatedpackage.jsonto include the new build process and output files. [1] [2]Documentation & Branding
wormsCTRLthroughout all documentation and metadata, including repository links inAGENTS.mdandSECURITY.md. [1] [2] [3]README.mdwith new architecture diagrams, quick start, threat DB overview, AI ingestion explanation, and grant context.Threat Catalog & AI Ingestion
packages/engine/src/ingest.tsand related files) for extracting structured threat objects from advisories using OpenAI and Zod validation.Bug Fixes, Security, and DX Improvements
These changes collectively deliver a more robust, secure, and user-friendly supply chain audit tool, with a focus on automation, AI integration, and real-world threat coverage.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests