Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
26 changes: 12 additions & 14 deletions packages/engine/src/types.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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[]
Expand Down
3 changes: 3 additions & 0 deletions packages/engine/tests/osv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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) {
Expand Down
7 changes: 6 additions & 1 deletion packages/ioc/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
Expand Down
8 changes: 5 additions & 3 deletions packages/scanner/src/detectors/injection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ function loadPackageJsonDeps(targetDir: string): Set<string> | 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<string>()
}
}

Expand Down Expand Up @@ -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,
Expand Down
6 changes: 2 additions & 4 deletions packages/scanner/src/output/sarif.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,15 @@ function severityToSarifLevel(severity: Finding['severity']): 'error' | 'warning

/** Generate SARIF ruleId from finding type */
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]
}

/** Convert finding location string to SARIF location */
Expand Down
14 changes: 12 additions & 2 deletions packages/scanner/src/parsers/bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 []
Expand Down
1 change: 1 addition & 0 deletions packages/scanner/src/parsers/pnpm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface PnpmPackageEntry {
interface PnpmLockfile {
lockfileVersion?: number | string
packages?: Record<string, PnpmPackageEntry>
snapshots?: Record<string, PnpmPackageEntry>
}

export interface ParsedPackage extends LockfilePackage {}
Expand Down
Loading