feat: prepare CASAN paid PoC release package
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
import { Controller, Get, Inject, Param } from '@nestjs/common';
|
||||
import { ok } from '../common/api-response.js';
|
||||
import { EvidenceService } from './evidence.service.js';
|
||||
|
||||
@Controller('api/v1/evidence-packs')
|
||||
export class EvidenceController {
|
||||
constructor(@Inject(EvidenceService) private readonly service: EvidenceService) {}
|
||||
|
||||
@Get()
|
||||
list() { return ok(this.service.list()); }
|
||||
|
||||
@Get(':id')
|
||||
get(@Param('id') id: string) { return ok(this.service.get(id)); }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { EvidenceController } from './evidence.controller.js';
|
||||
import { EvidenceService } from './evidence.service.js';
|
||||
|
||||
@Module({ controllers: [EvidenceController], providers: [EvidenceService] })
|
||||
export class EvidenceModule {}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { basename, join, relative, resolve } from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { APP_ROOT, PATHS } from '../common/app-root.js';
|
||||
|
||||
const PACK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
||||
const JSON_LIMIT = 1024 * 1024;
|
||||
const TEXT_LIMIT = 256 * 1024;
|
||||
const REPORTS = [
|
||||
'h1-context-report.json', 'h2-tool-audit.json', 'h3-eval-scorecard.json',
|
||||
'h4-security-report.json', 'h5-audit-chain-proof.json', 'h6-cost-telemetry.json',
|
||||
'h7-orchestration-report.json', 'redteam-result.json', 'benign-fp-report.json',
|
||||
];
|
||||
|
||||
function isInside(root: string, path: string) {
|
||||
const rel = relative(root, path);
|
||||
return rel === '' || (!rel.startsWith('..') && !rel.includes(`..${String.fromCharCode(47)}`));
|
||||
}
|
||||
|
||||
function readBounded(path: string, max: number): string | null {
|
||||
try {
|
||||
if (statSync(path).size > max) return null;
|
||||
return readFileSync(path, 'utf8');
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
function readObject(path: string): Record<string, unknown> | null {
|
||||
const source = readBounded(path, JSON_LIMIT);
|
||||
if (!source) return null;
|
||||
try {
|
||||
const value: unknown = JSON.parse(source);
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class EvidenceService {
|
||||
private readonly root = resolve(PATHS.evidencePacks);
|
||||
|
||||
private pack(id: string): string {
|
||||
if (!PACK_ID.test(id)) throw new NotFoundException('Evidence pack not found');
|
||||
const path = resolve(this.root, id);
|
||||
if (!isInside(this.root, path) || basename(path) !== id || !existsSync(path)) {
|
||||
throw new NotFoundException('Evidence pack not found');
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private integrity(dir: string) {
|
||||
const script = join(APP_ROOT, 'packages', 'casan-harness', 'scripts', 'bash', 'evidence-pack-verify.py');
|
||||
if (!existsSync(script)) return { ok: false, status: 'verifier_unavailable', output: 'Evidence verifier is unavailable.' };
|
||||
try {
|
||||
const output = execFileSync('python3', [script, dir], { cwd: APP_ROOT, encoding: 'utf8', timeout: 15_000, stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
||||
return { ok: true, status: 'verified', output };
|
||||
} catch (error: unknown) {
|
||||
const detail = error && typeof error === 'object' && 'stderr' in error ? String((error as { stderr?: unknown }).stderr ?? '') : '';
|
||||
return { ok: false, status: 'failed', output: detail.trim().slice(0, 500) || 'Evidence manifest verification failed.' };
|
||||
}
|
||||
}
|
||||
|
||||
private summary(id: string, dir: string) {
|
||||
const run = readObject(join(dir, 'run-summary.json')) ?? {};
|
||||
const manifest = readObject(join(dir, 'artifact-manifest.json')) ?? {};
|
||||
const signature = existsSync(join(dir, 'evidence-pack.sig')) ? 'present' : 'unsigned';
|
||||
return {
|
||||
run_id: String(run.run_id ?? id),
|
||||
certified: run.certified === true,
|
||||
certification_reasons: Array.isArray(run.certification_reasons) ? run.certification_reasons.map(String) : [],
|
||||
pack_version: String(run.pack_version ?? 'unknown'),
|
||||
created_at: statSync(dir).mtime.toISOString(),
|
||||
file_count: manifest.files && typeof manifest.files === 'object' && !Array.isArray(manifest.files) ? Object.keys(manifest.files as Record<string, unknown>).length : 0,
|
||||
signature,
|
||||
integrity: this.integrity(dir),
|
||||
};
|
||||
}
|
||||
|
||||
list() {
|
||||
if (!existsSync(this.root)) return { root: this.root, packs: [] };
|
||||
const packs = readdirSync(this.root, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory() && PACK_ID.test(entry.name))
|
||||
.map((entry) => this.summary(entry.name, join(this.root, entry.name)))
|
||||
.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
||||
return { root: this.root, packs };
|
||||
}
|
||||
|
||||
get(id: string) {
|
||||
const dir = this.pack(id);
|
||||
const manifest = readObject(join(dir, 'artifact-manifest.json')) ?? {};
|
||||
const hashes = manifest.files && typeof manifest.files === 'object' && !Array.isArray(manifest.files)
|
||||
? manifest.files as Record<string, unknown> : {};
|
||||
const files = Object.entries(hashes).map(([path, sha256]) => ({
|
||||
path,
|
||||
sha256: String(sha256),
|
||||
bytes: (() => { try { return statSync(join(dir, path)).size; } catch { return 0; } })(),
|
||||
}));
|
||||
const reports = Object.fromEntries(REPORTS.map((name) => [name, readObject(join(dir, name))]).filter(([, value]) => value));
|
||||
return {
|
||||
...this.summary(id, dir),
|
||||
manifest_head: readBounded(join(dir, 'manifest-head.txt'), 512)?.trim() || String(manifest.manifest_head ?? '') || null,
|
||||
run_summary: readObject(join(dir, 'run-summary.json')),
|
||||
decision_log: readBounded(join(dir, 'decision-log.md'), TEXT_LIMIT),
|
||||
reports,
|
||||
files,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user