feat: prepare CASAN paid PoC release package

This commit is contained in:
thanhnv
2026-07-18 00:07:04 +07:00
parent c818eaf8b0
commit 881ee01691
55 changed files with 5992 additions and 44 deletions
@@ -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,
};
}
}