diff --git a/package.json b/package.json index b9bef71..bccb2c8 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "homepage": "https://github.com/miccy/worms-ctrl#readme", "packageManager": "bun@1.1.38", "engines": { - "node": ">=18" + "node": ">=20.19" }, "overrides": { "vite": "8.0.10" diff --git a/packages/engine/src/types.ts b/packages/engine/src/types.ts index afdda13..e2035d8 100644 --- a/packages/engine/src/types.ts +++ b/packages/engine/src/types.ts @@ -1,16 +1,14 @@ export type FeedSource = 'osv' | 'socket' | 'github' | 'phylum' | 'npm-replicate' | 'rss' -export type ThreatEcosystem = - | 'npm' - | 'pypi' - | 'cargo' - | 'rubygems' - | 'gem' - | 'linux' - | 'maven' - | 'nuget' -export type ThreatSeverity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' -export type ThreatStatus = 'ACTIVE' | 'PATCHED' | 'ARCHIVED' | 'UNDER_REVIEW' +export type ThreatEcosystem = 'npm' | 'pypi' | 'cargo' | 'rubygems' | 'linux' + +export type ThreatProfileEcosystem = ThreatEcosystem | 'gem' | 'maven' | 'nuget' + +export type ThreatSeverity = 'CRITICAL' | 'HIGH' | 'MEDIUM' +export type ThreatProfileSeverity = ThreatSeverity | 'LOW' + +export type ThreatStatus = 'ACTIVE' | 'PATCHED' | 'ARCHIVED' +export type ThreatProfileStatus = ThreatStatus | 'UNDER_REVIEW' export interface ThreatMaliciousVersion { package: string @@ -66,9 +64,9 @@ export interface IOC { export interface ThreatProfile { id: string name: string - ecosystem: ThreatEcosystem - severity: ThreatSeverity - status: ThreatStatus + ecosystem: ThreatProfileEcosystem + severity: ThreatProfileSeverity + status: ThreatProfileStatus iocs: IOC[] ttp?: string[] // MITRE ATT&CK IDs references: string[] diff --git a/packages/engine/tests/osv.test.ts b/packages/engine/tests/osv.test.ts index c09fe3e..fe090d5 100644 --- a/packages/engine/tests/osv.test.ts +++ b/packages/engine/tests/osv.test.ts @@ -24,6 +24,8 @@ describe('osvToThreatProfile', () => { 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) }) test('maps CVSS_V3 scores to severity correctly', () => { @@ -36,6 +38,7 @@ describe('osvToThreatProfile', () => { { score: '4.0', expected: 'MEDIUM' }, { score: '3.9', expected: 'LOW' }, { score: '0.0', expected: 'LOW' }, + { score: 'N/A', expected: 'LOW' }, ] for (const { score, expected } of testCases) { diff --git a/packages/ioc/index.js b/packages/ioc/index.js index f1e99cc..e4d787d 100644 --- a/packages/ioc/index.js +++ b/packages/ioc/index.js @@ -53,7 +53,12 @@ function loadThreatCatalog(forceReload = false) { for (const entry of entries) { try { const content = readFileSync(join(THREATS_DIR, entry), 'utf8') - threats.push(JSON.parse(content)) + const parsed = JSON.parse(content) + if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) && 'id' in parsed && 'status' in parsed) { + threats.push(parsed) + } else { + console.warn(`[ioc] Skipping invalid threat file ${entry}: missing required fields or not an object`) + } } catch (error) { const message = error instanceof Error ? error.message : 'unknown error' console.warn(`[ioc] Skipping malformed threat file ${entry}: ${message}`) diff --git a/packages/scanner/src/detectors/injection.ts b/packages/scanner/src/detectors/injection.ts index 0109858..9d696fd 100644 --- a/packages/scanner/src/detectors/injection.ts +++ b/packages/scanner/src/detectors/injection.ts @@ -36,8 +36,10 @@ function loadPackageJsonDeps(targetDir: string): Set | null { } } return declared - } catch { - return null + } catch (error) { + const message = error instanceof Error ? error.message : 'unknown error' + console.warn(`[detectInjection] loadPackageJsonDeps failed for ${targetDir}: ${message}`) + return new Set() } } @@ -75,7 +77,7 @@ export function detectInjection( for (const match of phantomMatches) { findings.push({ type: 'injection', - severity: 'critical', + severity: toFindingSeverity(match.threat.severity), package: pkg.name, message: `Phantom dependency: ${pkg.name}@${version} — not declared in package.json, matches known IOC`, location, diff --git a/packages/scanner/src/output/sarif.ts b/packages/scanner/src/output/sarif.ts index ca0fecd..96bf688 100644 --- a/packages/scanner/src/output/sarif.ts +++ b/packages/scanner/src/output/sarif.ts @@ -60,7 +60,7 @@ function severityToSarifLevel(severity: Finding['severity']): 'error' | 'warning /** Generate SARIF ruleId from finding type */ function ruleId(finding: Finding): string { - const map: Record = { + const map: Record = { injection: 'WCTRL/scan/injected-package', 'hash-mismatch': 'WCTRL/scan/hash-mismatch', doppelganger: 'WCTRL/scan/doppelganger', @@ -68,9 +68,7 @@ function ruleId(finding: Finding): string { '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] } /** Convert finding location string to SARIF location */ diff --git a/packages/scanner/src/parsers/bun.ts b/packages/scanner/src/parsers/bun.ts index 5bcaf0f..e4a5220 100644 --- a/packages/scanner/src/parsers/bun.ts +++ b/packages/scanner/src/parsers/bun.ts @@ -10,6 +10,10 @@ function resolveBunLockTextPath(targetOrPath: string): string | null { if (basename(targetOrPath) === 'bun.lock') { return existsSync(targetOrPath) ? targetOrPath : null } + if (basename(targetOrPath) === 'bun.lockb') { + const candidate = join(dirname(targetOrPath), 'bun.lock') + return existsSync(candidate) ? candidate : null + } const candidate = join(targetOrPath, 'bun.lock') return existsSync(candidate) ? candidate : null @@ -203,8 +207,14 @@ export function parseBunLockfile(targetDirOrPath: string): ParsedPackage[] { } } - const bunLockBinaryPath = - basename(targetDirOrPath) === 'bun.lockb' ? targetDirOrPath : join(targetDirOrPath, 'bun.lockb') + let bunLockBinaryPath = '' + if (basename(targetDirOrPath) === 'bun.lockb') { + bunLockBinaryPath = targetDirOrPath + } else if (basename(targetDirOrPath) === 'bun.lock') { + bunLockBinaryPath = join(dirname(targetDirOrPath), 'bun.lockb') + } else { + bunLockBinaryPath = join(targetDirOrPath, 'bun.lockb') + } if (!existsSync(bunLockBinaryPath)) { return [] diff --git a/packages/scanner/src/parsers/pnpm.ts b/packages/scanner/src/parsers/pnpm.ts index a5488e4..856eb33 100644 --- a/packages/scanner/src/parsers/pnpm.ts +++ b/packages/scanner/src/parsers/pnpm.ts @@ -15,6 +15,7 @@ interface PnpmPackageEntry { interface PnpmLockfile { lockfileVersion?: number | string packages?: Record + snapshots?: Record } export interface ParsedPackage extends LockfilePackage {}