Initial commit: omp-security DSH plugin (oh-my-pi port) with docs and tests

This commit is contained in:
Agent committed 2026-10-10 21:30:02 +00:00
commit e37410e516
9 files changed
+627

No files matched your search

+164
View File
@@ -0,0 +1,164 @@
// Scanner adapters: each returns normalized findings
// {id, scanner, rule, severity: critical|high|medium|low|info, title, file, line, endLine, snippet, cwe, fix, url}
import { execFile } from 'node:child_process'
import crypto from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
const HOME = process.env.HOME || ''
export function which(cmd) {
for (const d of [...(process.env.PATH || '').split(path.delimiter), path.join(HOME, '.local/bin'), '/usr/local/bin']) {
if (!d) continue
const p = path.join(d, cmd)
try { fs.accessSync(p, fs.constants.X_OK); return p } catch {}
}
return null
}
export function run(bin, args, { cwd, signal, timeoutMs = 600_000, env } = {}) {
return new Promise((resolve) => {
execFile(bin, args, { cwd, signal, timeout: timeoutMs, maxBuffer: 256 * 1024 * 1024, encoding: 'utf8', env: { ...process.env, ...env } }, (error, stdout, stderr) => {
resolve({ code: error ? (typeof error.code === 'number' ? error.code : -1) : 0, stdout: stdout ?? '', stderr: stderr ?? '', killed: !!error?.killed })
})
})
}
const fid = (...parts) => crypto.createHash('sha1').update(parts.join('|')).digest('hex').slice(0, 10)
const sev = (s) => {
const t = String(s ?? '').toLowerCase()
if (/crit/.test(t)) return 'critical'
if (/high|error/.test(t)) return 'high'
if (/med|warn|moderate/.test(t)) return 'medium'
if (/low|note/.test(t)) return 'low'
return 'info'
}
function hasFiles(root, exts, files) {
if (files) return files.some((f) => exts.includes(path.extname(f)))
try {
const stack = [root]; let n = 0
while (stack.length && n < 20000) {
const d = stack.pop()
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
if (e.name.startsWith('.') || e.name === 'node_modules' || e.name === 'venv' || e.name === 'dist') continue
const p = path.join(d, e.name); n++
if (e.isDirectory()) stack.push(p)
else if (exts.includes(path.extname(e.name))) return true
}
}
} catch {}
return false
}
export const SCANNERS = {
semgrep: {
what: 'SAST rules for 30+ languages (OWASP top 10, injection, crypto misuse, …)',
install: 'pip install --user semgrep',
available: () => !!which('semgrep'),
applies: () => true,
async scan({ root, files, signal, config }) {
const args = ['scan', '--json', '--quiet', '--disable-version-check', '--metrics=off', '--timeout', '30', `--config=${config?.semgrepConfig ?? 'p/default'}`]
for (const ex of config?.exclude ?? []) args.push('--exclude', ex)
args.push(...(files?.length ? files : ['.']))
const r = await run(which('semgrep'), args, { cwd: root, signal, env: { SEMGREP_SEND_METRICS: 'off' } })
let j
try { j = JSON.parse(r.stdout) } catch { throw new Error(`semgrep: ${r.stderr.split('\n').filter(Boolean).slice(-3).join(' ') || `exit ${r.code}`}`) }
return (j.results ?? []).map((x) => ({
id: fid('semgrep', x.check_id, x.path, x.start?.line),
scanner: 'semgrep', rule: x.check_id, severity: sev(x.extra?.metadata?.impact ?? x.extra?.severity),
title: x.extra?.message?.split('\n')[0] ?? x.check_id, file: x.path, line: x.start?.line, endLine: x.end?.line,
snippet: x.extra?.lines, cwe: [].concat(x.extra?.metadata?.cwe ?? []).join(', ') || undefined,
fix: x.extra?.fix, url: x.extra?.metadata?.source ?? x.extra?.metadata?.references?.[0],
}))
},
},
bandit: {
what: 'Python security linter',
install: 'pip install --user bandit',
available: () => !!which('bandit'),
applies: ({ root, files }) => hasFiles(root, ['.py'], files),
async scan({ root, files, signal }) {
const args = ['-f', 'json', '-q', ...(files?.length ? files.filter((f) => f.endsWith('.py')) : ['-r', '.', '-x', './node_modules,./.venv,./venv,./.git'])]
const r = await run(which('bandit'), args, { cwd: root, signal })
let j
try { j = JSON.parse(r.stdout) } catch { throw new Error(`bandit: ${r.stderr.slice(-300) || `exit ${r.code}`}`) }
return (j.results ?? []).map((x) => ({
id: fid('bandit', x.test_id, x.filename, x.line_number),
scanner: 'bandit', rule: `${x.test_id} ${x.test_name}`, severity: sev(x.issue_severity),
confidence: x.issue_confidence?.toLowerCase(), title: x.issue_text, file: path.relative(root, path.resolve(root, x.filename)),
line: x.line_number, endLine: x.line_range?.at(-1), snippet: x.code, cwe: x.issue_cwe?.id ? `CWE-${x.issue_cwe.id}` : undefined, url: x.more_info,
}))
},
},
gitleaks: {
what: 'hardcoded secrets (API keys, tokens, private keys)',
install: 'download from github.com/gitleaks/gitleaks/releases into ~/.local/bin',
available: () => !!which('gitleaks'),
applies: () => true,
async scan({ root, signal, target }) {
const report = path.join(fs.mkdtempSync(path.join(process.env.TMPDIR || '/tmp', 'gl-')), 'r.json')
const isGit = fs.existsSync(path.join(root, '.git'))
const args = target === 'repository' && isGit
? ['git', '--no-banner', '--report-format', 'json', '--report-path', report, '--redact', '.']
: ['dir', '--no-banner', '--report-format', 'json', '--report-path', report, '--redact', '.']
await run(which('gitleaks'), args, { cwd: root, signal })
let j = []
try { j = JSON.parse(fs.readFileSync(report, 'utf8')) } catch {}
fs.rmSync(path.dirname(report), { recursive: true, force: true })
return j.map((x) => ({
id: fid('gitleaks', x.RuleID, x.File, x.StartLine, x.Commit ?? ''),
scanner: 'gitleaks', rule: x.RuleID, severity: 'high', title: `${x.Description}${x.Commit ? ` (commit ${x.Commit.slice(0, 8)})` : ''}`,
file: x.File, line: x.StartLine, endLine: x.EndLine, snippet: x.Match, cwe: 'CWE-798',
}))
},
},
'osv-scanner': {
what: 'known vulnerabilities in dependencies (lockfiles: npm, pnpm, yarn, pip, poetry, go, cargo, maven, …)',
install: 'download from github.com/google/osv-scanner/releases into ~/.local/bin',
available: () => !!which('osv-scanner'),
applies: () => true,
async scan({ root, signal }) {
const r = await run(which('osv-scanner'), ['scan', 'source', '--format', 'json', '-r', '.'], { cwd: root, signal })
let j
try { j = JSON.parse(r.stdout) } catch { if (/no package sources found/i.test(r.stderr)) return []; throw new Error(`osv-scanner: ${r.stderr.split('\n').filter(Boolean).slice(-2).join(' ') || `exit ${r.code}`}`) }
const out = []
for (const res of j.results ?? []) {
for (const pkg of res.packages ?? []) {
for (const v of pkg.vulnerabilities ?? []) {
const group = (pkg.groups ?? []).find((g) => g.ids?.includes(v.id))
const score = Number(group?.max_severity)
const severity = Number.isFinite(score) ? (score >= 9 ? 'critical' : score >= 7 ? 'high' : score >= 4 ? 'medium' : 'low') : sev(v.database_specific?.severity)
const fixed = (v.affected ?? []).flatMap((a) => (a.ranges ?? []).flatMap((rg) => (rg.events ?? []).map((e) => e.fixed).filter(Boolean)))
out.push({
id: fid('osv', v.id, pkg.package?.name, res.source?.path),
scanner: 'osv-scanner', rule: v.id, severity,
title: `${pkg.package?.name}@${pkg.package?.version}: ${v.summary ?? v.id}`,
file: path.relative(root, res.source?.path ?? ''), fix: fixed.length ? `upgrade to ${fixed[0]}` : undefined,
url: `https://osv.dev/vulnerability/${v.id}`, cwe: (v.database_specific?.cwe_ids ?? []).join(', ') || undefined,
})
}
}
}
return out
},
},
'npm-audit': {
what: 'npm advisory database for package-lock.json projects',
install: 'bundled with npm',
available: () => !!which('npm'),
applies: ({ root }) => fs.existsSync(path.join(root, 'package-lock.json')),
async scan({ root, signal }) {
const r = await run(which('npm'), ['audit', '--json'], { cwd: root, signal })
let j
try { j = JSON.parse(r.stdout) } catch { return [] }
return Object.values(j.vulnerabilities ?? {}).map((v) => ({
id: fid('npm', v.name, v.range),
scanner: 'npm-audit', rule: (v.via ?? []).map((x) => (typeof x === 'object' ? x.source ?? x.url : x)).join(', '),
severity: sev(v.severity), title: `${v.name} ${v.range}: ${(v.via ?? []).map((x) => (typeof x === 'object' ? x.title : `via ${x}`)).join('; ')}`,
file: 'package-lock.json', fix: v.fixAvailable ? (typeof v.fixAvailable === 'object' ? `upgrade ${v.fixAvailable.name} to ${v.fixAvailable.version}` : 'npm audit fix') : undefined,
}))
},
},
}
export const SEV_ORDER = ['critical', 'high', 'medium', 'low', 'info']