A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
229 lines
6.6 KiB
TypeScript
229 lines
6.6 KiB
TypeScript
/**
|
|
* Scan command - Run security scans
|
|
*/
|
|
|
|
import { Command } from 'commander'
|
|
import * as path from 'path'
|
|
|
|
interface ScanOptions {
|
|
tools?: string
|
|
output?: string
|
|
format?: string
|
|
severity?: string
|
|
}
|
|
|
|
export const scanCommand = new Command('scan')
|
|
.description('Run security scans on your codebase')
|
|
.argument('[path]', 'Path to scan', '.')
|
|
.option('-t, --tools <tools>', 'Comma-separated list of tools (gitleaks,semgrep,bandit,trivy,grype,syft)', 'all')
|
|
.option('-o, --output <path>', 'Output file path')
|
|
.option('-f, --format <format>', 'Output format (json, sarif, table)', 'table')
|
|
.option('-s, --severity <level>', 'Minimum severity to report (low, medium, high, critical)', 'low')
|
|
.action(async (scanPath: string, options: ScanOptions) => {
|
|
const chalk = (await import('chalk')).default
|
|
const ora = (await import('ora')).default
|
|
|
|
console.log(chalk.bold.blue('\n🔒 BreakPilot Security Scanner\n'))
|
|
|
|
const targetPath = path.resolve(scanPath)
|
|
console.log(chalk.gray(`Scanning: ${targetPath}\n`))
|
|
|
|
const tools = options.tools === 'all'
|
|
? ['gitleaks', 'semgrep', 'bandit', 'trivy', 'grype', 'syft']
|
|
: options.tools!.split(',').map(t => t.trim())
|
|
|
|
const results: ScanResult[] = []
|
|
|
|
for (const tool of tools) {
|
|
const spinner = ora(`Running ${tool}...`).start()
|
|
|
|
try {
|
|
// Simulate running the tool
|
|
await sleep(1500)
|
|
|
|
const toolResult = simulateToolResult(tool)
|
|
results.push(toolResult)
|
|
|
|
if (toolResult.findings.length > 0) {
|
|
spinner.warn(`${tool}: ${toolResult.findings.length} findings`)
|
|
} else {
|
|
spinner.succeed(`${tool}: No issues found`)
|
|
}
|
|
} catch (error) {
|
|
spinner.fail(`${tool}: Failed`)
|
|
}
|
|
}
|
|
|
|
// Summary
|
|
console.log(chalk.bold('\n📊 Scan Summary\n'))
|
|
|
|
const allFindings = results.flatMap(r => r.findings)
|
|
const bySeverity = {
|
|
critical: allFindings.filter(f => f.severity === 'CRITICAL'),
|
|
high: allFindings.filter(f => f.severity === 'HIGH'),
|
|
medium: allFindings.filter(f => f.severity === 'MEDIUM'),
|
|
low: allFindings.filter(f => f.severity === 'LOW'),
|
|
}
|
|
|
|
if (bySeverity.critical.length > 0) {
|
|
console.log(chalk.red(` 🔴 Critical: ${bySeverity.critical.length}`))
|
|
}
|
|
if (bySeverity.high.length > 0) {
|
|
console.log(chalk.red(` 🟠 High: ${bySeverity.high.length}`))
|
|
}
|
|
if (bySeverity.medium.length > 0) {
|
|
console.log(chalk.yellow(` 🟡 Medium: ${bySeverity.medium.length}`))
|
|
}
|
|
if (bySeverity.low.length > 0) {
|
|
console.log(chalk.gray(` 🟢 Low: ${bySeverity.low.length}`))
|
|
}
|
|
|
|
if (allFindings.length === 0) {
|
|
console.log(chalk.green(' ✅ No security issues found!'))
|
|
} else {
|
|
console.log(chalk.gray(`\n Total: ${allFindings.length} findings`))
|
|
|
|
// Show top findings
|
|
if (options.format === 'table') {
|
|
console.log(chalk.bold('\n📋 Top Findings\n'))
|
|
|
|
const topFindings = allFindings
|
|
.sort((a, b) => severityOrder(b.severity) - severityOrder(a.severity))
|
|
.slice(0, 10)
|
|
|
|
for (const finding of topFindings) {
|
|
const severityColor = getSeverityColor(finding.severity)
|
|
console.log(
|
|
` ${severityColor(finding.severity.padEnd(8))} ` +
|
|
`${chalk.gray(finding.tool.padEnd(10))} ` +
|
|
`${finding.title}`
|
|
)
|
|
if (finding.file) {
|
|
console.log(chalk.gray(` └─ ${finding.file}:${finding.line || '?'}`))
|
|
}
|
|
}
|
|
|
|
if (allFindings.length > 10) {
|
|
console.log(chalk.gray(`\n ... and ${allFindings.length - 10} more findings`))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write output if requested
|
|
if (options.output) {
|
|
const fs = await import('fs')
|
|
const output = {
|
|
scanDate: new Date().toISOString(),
|
|
targetPath,
|
|
tools,
|
|
summary: {
|
|
total: allFindings.length,
|
|
critical: bySeverity.critical.length,
|
|
high: bySeverity.high.length,
|
|
medium: bySeverity.medium.length,
|
|
low: bySeverity.low.length,
|
|
},
|
|
findings: allFindings,
|
|
}
|
|
|
|
fs.writeFileSync(options.output, JSON.stringify(output, null, 2))
|
|
console.log(chalk.gray(`\nResults written to: ${options.output}`))
|
|
}
|
|
|
|
// Exit with error if critical findings
|
|
if (bySeverity.critical.length > 0 || bySeverity.high.length > 0) {
|
|
process.exit(1)
|
|
}
|
|
})
|
|
|
|
interface Finding {
|
|
id: string
|
|
tool: string
|
|
severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'
|
|
title: string
|
|
description?: string
|
|
file?: string
|
|
line?: number
|
|
recommendation?: string
|
|
}
|
|
|
|
interface ScanResult {
|
|
tool: string
|
|
findings: Finding[]
|
|
}
|
|
|
|
function simulateToolResult(tool: string): ScanResult {
|
|
// Simulate some findings for demonstration
|
|
const findings: Finding[] = []
|
|
|
|
switch (tool) {
|
|
case 'gitleaks':
|
|
// Secrets detection - usually finds nothing in clean repos
|
|
break
|
|
case 'semgrep':
|
|
findings.push({
|
|
id: 'semgrep-1',
|
|
tool: 'semgrep',
|
|
severity: 'MEDIUM',
|
|
title: 'Potential SQL injection',
|
|
description: 'User input used in SQL query without parameterization',
|
|
file: 'src/db/queries.ts',
|
|
line: 42,
|
|
recommendation: 'Use parameterized queries instead of string concatenation',
|
|
})
|
|
break
|
|
case 'bandit':
|
|
// Python security - skip if not a Python project
|
|
break
|
|
case 'trivy':
|
|
findings.push({
|
|
id: 'trivy-1',
|
|
tool: 'trivy',
|
|
severity: 'HIGH',
|
|
title: 'CVE-2024-1234 in lodash@4.17.20',
|
|
description: 'Prototype pollution vulnerability',
|
|
recommendation: 'Upgrade to lodash@4.17.21 or higher',
|
|
})
|
|
break
|
|
case 'grype':
|
|
findings.push({
|
|
id: 'grype-1',
|
|
tool: 'grype',
|
|
severity: 'LOW',
|
|
title: 'Outdated dependency: axios@0.21.0',
|
|
recommendation: 'Update to latest version',
|
|
})
|
|
break
|
|
case 'syft':
|
|
// SBOM generation - no findings, just metadata
|
|
break
|
|
}
|
|
|
|
return { tool, findings }
|
|
}
|
|
|
|
function severityOrder(severity: string): number {
|
|
switch (severity) {
|
|
case 'CRITICAL': return 4
|
|
case 'HIGH': return 3
|
|
case 'MEDIUM': return 2
|
|
case 'LOW': return 1
|
|
default: return 0
|
|
}
|
|
}
|
|
|
|
function getSeverityColor(severity: string): (text: string) => string {
|
|
const chalk = require('chalk')
|
|
switch (severity) {
|
|
case 'CRITICAL': return chalk.red.bold
|
|
case 'HIGH': return chalk.red
|
|
case 'MEDIUM': return chalk.yellow
|
|
case 'LOW': return chalk.gray
|
|
default: return chalk.white
|
|
}
|
|
}
|
|
|
|
function sleep(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
}
|