From 354dd2b2effa87d44e182e992cbcd0f47ae7f88f Mon Sep 17 00:00:00 2001 From: miccy <9729864+miccy@users.noreply.github.com> Date: Tue, 5 May 2026 05:10:44 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B9=20Resolve=20remaining=20nitpicks?= =?UTF-8?q?=20and=20additional=20PR=20#24=20code=20review=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/docs/playwright.config.ts | 9 ++----- package.json | 2 +- packages/engine/src/types.ts | 26 ++++++++++----------- packages/engine/tests/osv.test.ts | 21 ++++++++++------- packages/ioc/index.js | 7 +++++- packages/scanner/src/detectors/injection.ts | 17 ++++++++++---- packages/scanner/src/output/sarif.ts | 6 ++--- packages/scanner/src/parsers/bun.ts | 14 +++++++++-- packages/scanner/src/parsers/js-yaml.d.ts | 17 ++++++-------- packages/scanner/src/parsers/pnpm.ts | 1 + packages/wiki-sync/src/index.ts | 2 +- 11 files changed, 69 insertions(+), 53 deletions(-) diff --git a/apps/docs/playwright.config.ts b/apps/docs/playwright.config.ts index 777a616..627fd37 100644 --- a/apps/docs/playwright.config.ts +++ b/apps/docs/playwright.config.ts @@ -19,14 +19,9 @@ export default defineConfig({ }, ], webServer: { - command: process.env.CI ? 'bun run build && bun run preview' : 'bun run dev', + command: 'bun run build && bun run preview', url: 'http://localhost:4321', reuseExistingServer: !process.env.CI, - timeout: (() => { - const parsed = Number.parseInt(process.env.PLAYWRIGHT_STARTUP_TIMEOUT || '', 10) - return Number.isNaN(parsed) ? 120_000 : parsed - })(), - stdout: 'pipe', - stderr: 'pipe', + timeout: 120000, }, }) 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 1d88a28..fe090d5 100644 --- a/packages/engine/tests/osv.test.ts +++ b/packages/engine/tests/osv.test.ts @@ -18,12 +18,14 @@ describe('osvToThreatProfile', () => { } test('converts basic OSV record correctly', () => { - const result = osvToThreatProfile(baseOsv) as any + const result = osvToThreatProfile(baseOsv) as unknown as Record 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) }) 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) { @@ -43,7 +46,7 @@ describe('osvToThreatProfile', () => { ...baseOsv, severity: [{ type: 'CVSS_V3', score }], } - const result = osvToThreatProfile(osv) as any + const result = osvToThreatProfile(osv) as unknown as Record expect(result.severity).toBe(expected) } }) @@ -53,7 +56,7 @@ describe('osvToThreatProfile', () => { ...baseOsv, severity: [{ type: 'CVSS_V2', score: '10.0' }], } - const result = osvToThreatProfile(osv) as any + const result = osvToThreatProfile(osv) as unknown as Record expect(result.severity).toBe('LOW') }) @@ -62,7 +65,7 @@ describe('osvToThreatProfile', () => { ...baseOsv, severity: undefined, } - const result = osvToThreatProfile(osv) as any + const result = osvToThreatProfile(osv) as unknown as Record expect(result.severity).toBe('LOW') }) @@ -74,7 +77,7 @@ describe('osvToThreatProfile', () => { { package: { name: 'pkg2', ecosystem: 'npm' } }, ], } - const result = osvToThreatProfile(osv) as any + const result = osvToThreatProfile(osv) as unknown as Record expect(result.name).toBe('pkg1') expect(result.ecosystem).toBe('pypi') }) @@ -84,7 +87,7 @@ describe('osvToThreatProfile', () => { ...baseOsv, affected: [], } - const result = osvToThreatProfile(osv) as any + const result = osvToThreatProfile(osv) as unknown as Record expect(result.name).toBe(osv.id) expect(result.ecosystem).toBe('npm') }) @@ -94,7 +97,7 @@ describe('osvToThreatProfile', () => { ...baseOsv, summary: undefined, } - const result = osvToThreatProfile(osv) as any + const result = osvToThreatProfile(osv) as unknown as Record expect(result.description).toBe('') }) @@ -106,7 +109,7 @@ describe('osvToThreatProfile', () => { { type: 'WEB', url: 'https://example.com/web' }, ], } - const result = osvToThreatProfile(osv) as any + const result = osvToThreatProfile(osv) as unknown as Record expect(result.references).toEqual([ { type: 'ADVISORY', url: 'https://example.com/advisory' }, { type: 'WEB', url: 'https://example.com/web' }, @@ -118,7 +121,7 @@ describe('osvToThreatProfile', () => { ...baseOsv, references: undefined, } - const result = osvToThreatProfile(osv) as any + const result = osvToThreatProfile(osv) as unknown as Record expect(result.references).toEqual([]) }) }) 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 ffa6319..9d696fd 100644 --- a/packages/scanner/src/detectors/injection.ts +++ b/packages/scanner/src/detectors/injection.ts @@ -11,6 +11,8 @@ import { validatePath } from '../utils.js' interface PackageJsonManifest { dependencies?: Record devDependencies?: Record + peerDependencies?: Record + optionalDependencies?: Record } /** @@ -23,14 +25,21 @@ function loadPackageJsonDeps(targetDir: string): Set | null { readFileSync(resolve(targetDir, 'package.json'), 'utf-8') ) as PackageJsonManifest const declared = new Set() - for (const deps of [pkg.dependencies ?? {}, pkg.devDependencies ?? {}]) { + for (const deps of [ + pkg.dependencies ?? {}, + pkg.devDependencies ?? {}, + pkg.peerDependencies ?? {}, + pkg.optionalDependencies ?? {}, + ]) { for (const name of Object.keys(deps)) { declared.add(name) } } 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() } } @@ -68,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/js-yaml.d.ts b/packages/scanner/src/parsers/js-yaml.d.ts index afd8149..3a614fd 100644 --- a/packages/scanner/src/parsers/js-yaml.d.ts +++ b/packages/scanner/src/parsers/js-yaml.d.ts @@ -1,14 +1,11 @@ declare module 'js-yaml' { export interface LoadOptions { - filename?: string; - onWarning?: (warning: Error) => void; - schema?: any; - json?: boolean; - listener?: (eventType: string, state: any) => void; + filename?: string + onWarning?: (warning: Error) => void + schema?: unknown + json?: boolean + listener?: (eventType: string, state: unknown) => void } - - /** @deprecated Unsafe for untrusted input. Use safeLoad or supply a safe schema. */ - export function load(source: string, options?: LoadOptions): unknown; - - export function safeLoad(source: string, options?: LoadOptions): unknown; + + export function load(source: string, options?: LoadOptions): unknown } 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 {} diff --git a/packages/wiki-sync/src/index.ts b/packages/wiki-sync/src/index.ts index b7ba4e1..072a3b1 100644 --- a/packages/wiki-sync/src/index.ts +++ b/packages/wiki-sync/src/index.ts @@ -185,7 +185,7 @@ function generateSidebar(docs: DocFile[], lang: 'en' | 'cs'): string { */ function generateFooter(): string { return `--- -📖 [Documentation](https://hulud.dev) | 🐙 [GitHub](https://github.com/miccy/wormsCTRL) | 🪱 v1.5.1 +📖 [Documentation](https://hulud.dev) | 🐙 [GitHub](https://github.com/miccy/wormsCTRL) | 🪱 v2.0.0 ` }