Spaces:
Running
Running
| import { TextDecoder } from 'node:util' | |
| import { isAbsolute, join } from 'node:path' | |
| import { pathToFileURL } from 'node:url' | |
| const DOCUMENT_TYPE = 'whitehack-flashlight/v0.1' | |
| const SCANNER_NAME = 'whitehack' | |
| const SCANNER_VERSION = '0.9.0' | |
| const SCANNER_CHECK_COUNT = 47 | |
| const MAX_UTF8_BYTES = 65_536 | |
| const MAX_LINES = 2_000 | |
| const MAX_FINDINGS = 500 | |
| const MAX_REQUEST_BYTES = 512 * 1024 | |
| const LANGUAGES = Object.freeze({ | |
| javascript: Object.freeze({ core: 'js', rules: 43 }), | |
| python: Object.freeze({ core: 'py', rules: 21 }), | |
| solidity: Object.freeze({ core: 'sol', rules: 10 }), | |
| }) | |
| const CONFIDENCES = Object.freeze(['high', 'medium-high', 'medium', 'heuristic']) | |
| const LIMITS = Object.freeze({ | |
| max_utf8_bytes: MAX_UTF8_BYTES, | |
| max_lines: MAX_LINES, | |
| max_findings: MAX_FINDINGS, | |
| timeout_seconds: 3.0, | |
| }) | |
| const INTERPRETATION = Object.freeze({ | |
| finding: 'review_prompt_not_vulnerability_verdict', | |
| empty: 'not_proof_of_safety', | |
| }) | |
| const PRIVACY = Object.freeze({ | |
| source_returned: false, | |
| snippets_returned: false, | |
| application_persistence: 'not_written_by_this_app', | |
| hosting_platform_retention: 'unknown', | |
| }) | |
| const UTF8 = new TextDecoder('utf-8', { fatal: true }) | |
| function scannerIdentity() { | |
| return { | |
| name: SCANNER_NAME, | |
| version: SCANNER_VERSION, | |
| check_count: SCANNER_CHECK_COUNT, | |
| } | |
| } | |
| function zeroSummary() { | |
| return { | |
| finding_count: 0, | |
| by_confidence: { | |
| high: 0, | |
| 'medium-high': 0, | |
| medium: 0, | |
| heuristic: 0, | |
| }, | |
| } | |
| } | |
| function errorResponse(code) { | |
| return { | |
| document_type: DOCUMENT_TYPE, | |
| status: 'error', | |
| complete: false, | |
| scanner: scannerIdentity(), | |
| limits: { ...LIMITS }, | |
| scope: null, | |
| summary: zeroSummary(), | |
| findings: [], | |
| interpretation: { ...INTERPRETATION }, | |
| privacy: { ...PRIVACY }, | |
| error: { code }, | |
| } | |
| } | |
| function emit(document) { | |
| process.stdout.write(JSON.stringify(document)) | |
| } | |
| async function readRequest() { | |
| const chunks = [] | |
| let observed = 0 | |
| for await (const chunk of process.stdin) { | |
| observed += chunk.byteLength | |
| if (observed > MAX_REQUEST_BYTES) throw new Error('request_too_large') | |
| chunks.push(chunk) | |
| } | |
| return UTF8.decode(Buffer.concat(chunks)) | |
| } | |
| function parseRequest(raw) { | |
| const request = JSON.parse(raw) | |
| if ( | |
| request === null | |
| || typeof request !== 'object' | |
| || Array.isArray(request) | |
| || Object.keys(request).sort().join(',') !== 'language,source' | |
| ) { | |
| throw new Error('invalid_request') | |
| } | |
| if (typeof request.source !== 'string') throw new Error('invalid_input') | |
| if (!Object.hasOwn(LANGUAGES, request.language)) throw new Error('unsupported_language') | |
| const byteCount = Buffer.byteLength(request.source, 'utf8') | |
| if (byteCount > MAX_UTF8_BYTES) throw new Error('input_byte_limit_exceeded') | |
| const lineCount = request.source.split('\n').length | |
| if (lineCount > MAX_LINES) throw new Error('input_line_limit_exceeded') | |
| if (request.source.trim().length === 0) throw new Error('input_empty') | |
| return { ...request, byteCount, lineCount } | |
| } | |
| function publicFindings(findings) { | |
| return findings | |
| .map((finding) => ({ | |
| line: finding.line, | |
| check: finding.check, | |
| title: finding.title, | |
| confidence: finding.confidence, | |
| doctrine: finding.doctrine, | |
| principle: finding.principle, | |
| })) | |
| .sort((left, right) => ( | |
| left.line - right.line | |
| || (left.check < right.check ? -1 : left.check > right.check ? 1 : 0) | |
| )) | |
| } | |
| function summarize(findings) { | |
| const byConfidence = Object.fromEntries(CONFIDENCES.map((confidence) => [confidence, 0])) | |
| for (const finding of findings) byConfidence[finding.confidence] += 1 | |
| return { | |
| finding_count: findings.length, | |
| by_confidence: byConfidence, | |
| } | |
| } | |
| async function main() { | |
| const packageRoot = process.argv[2] | |
| if ( | |
| process.argv.length !== 3 | |
| || typeof packageRoot !== 'string' | |
| || !isAbsolute(packageRoot) | |
| ) { | |
| emit(errorResponse('scanner_identity_mismatch')) | |
| return | |
| } | |
| const nodeMajor = Number.parseInt(process.versions.node.split('.')[0], 10) | |
| if (!Number.isSafeInteger(nodeMajor) || nodeMajor < 18) { | |
| emit(errorResponse('scanner_runtime_unsupported')) | |
| return | |
| } | |
| let core | |
| try { | |
| core = await import(pathToFileURL(join(packageRoot, 'src', 'core.js')).href) | |
| } catch { | |
| emit(errorResponse('scanner_unavailable')) | |
| return | |
| } | |
| const { CHECK_MANIFEST, ScanTextError, scanText } = core | |
| if ( | |
| !Array.isArray(CHECK_MANIFEST) | |
| || CHECK_MANIFEST.length !== SCANNER_CHECK_COUNT | |
| || typeof scanText !== 'function' | |
| ) { | |
| emit(errorResponse('scanner_identity_mismatch')) | |
| return | |
| } | |
| let request | |
| try { | |
| request = parseRequest(await readRequest()) | |
| } catch (error) { | |
| const allowed = new Set([ | |
| 'invalid_request', | |
| 'invalid_input', | |
| 'unsupported_language', | |
| 'input_byte_limit_exceeded', | |
| 'input_line_limit_exceeded', | |
| 'input_empty', | |
| 'request_too_large', | |
| ]) | |
| emit(errorResponse(allowed.has(error?.message) ? error.message : 'invalid_request')) | |
| return | |
| } | |
| const language = LANGUAGES[request.language] | |
| const rulesConsidered = CHECK_MANIFEST.filter((check) => ( | |
| check.languages.length === 0 || check.languages.includes(language.core) | |
| )).length | |
| if (rulesConsidered !== language.rules) { | |
| emit(errorResponse('scanner_identity_mismatch')) | |
| return | |
| } | |
| let findings | |
| try { | |
| findings = publicFindings(scanText(request.source, { | |
| file: '[pasted-input]', | |
| lang: language.core, | |
| maxLines: MAX_LINES, | |
| maxFindings: MAX_FINDINGS, | |
| })) | |
| } catch (error) { | |
| if (error instanceof ScanTextError) { | |
| emit(errorResponse(error.code)) | |
| return | |
| } | |
| emit(errorResponse('scanner_failed')) | |
| return | |
| } | |
| emit({ | |
| document_type: DOCUMENT_TYPE, | |
| status: 'complete', | |
| complete: true, | |
| scanner: scannerIdentity(), | |
| limits: { ...LIMITS }, | |
| scope: { | |
| kind: 'caller-provided-text', | |
| language: request.language, | |
| utf8_bytes: request.byteCount, | |
| lines: request.lineCount, | |
| rules_considered: rulesConsidered, | |
| }, | |
| summary: summarize(findings), | |
| findings, | |
| interpretation: { ...INTERPRETATION }, | |
| privacy: { ...PRIVACY }, | |
| error: null, | |
| }) | |
| } | |
| await main() | |