Strengthen file scan evidence

This commit is contained in:
ZacharyZhang-NY committed 2026-05-23 23:16:40 -04:00
1 parent 9cc4c3b44a
commit 7c978c6a10
3 files changed
+165 -11

No files matched your search

@@ -4,16 +4,32 @@ import { basename, extname, join } from "node:path";
import type { DatabaseSync } from "node:sqlite";
import { dialog } from "electron";
import type { ImportFileInput } from "../../shared/commands";
import { FILE_SCAN_STATE } from "../../shared/domain";
import { FILE_SCAN_STATE, type FileScanState } from "../../shared/domain";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
interface FileScanResult {
state: typeof FILE_SCAN_STATE.APPROVED;
state: FileScanState;
labels: string[];
scannedAt: string;
}
interface TextScanPattern {
label: string;
pattern: RegExp;
block: boolean;
}
const TEXT_SCAN_PATTERNS: TextScanPattern[] = [
{ label: "dlp:private-key", pattern: /-----BEGIN (?:RSA |DSA |EC |OPENSSH |PRIVATE )?PRIVATE KEY-----/i, block: true },
{ label: "dlp:aws-access-key", pattern: /\bAKIA[0-9A-Z]{16}\b/, block: true },
{ label: "dlp:github-token", pattern: /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/, block: true },
{ label: "dlp:provider-secret", pattern: /\bsk-[A-Za-z0-9]{32,}\b/, block: true },
{ label: "dlp:credential", pattern: /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*["']?[A-Za-z0-9_\-./+=]{20,}/i, block: true },
{ label: "dlp:ssn", pattern: /\b\d{3}-\d{2}-\d{4}\b/, block: true },
{ label: "dlp:email", pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i, block: false },
];
function fileKind(path: string): string {
const ext = extname(path).toLowerCase().replace(".", "");
if (!ext) return "file";
@@ -91,7 +107,7 @@ export class FileService {
actorType: "system",
actorId: null,
action: "imported",
payload: { sha256: hash, sizeBytes: stats.size, scanState: scan.state },
payload: { sha256: hash, sizeBytes: stats.size, scanState: scan.state, scanLabels: scan.labels, objectRef: target },
});
this.db.exec("commit");
} catch (error) {
@@ -141,13 +157,26 @@ export class FileService {
}
function scanFile(sourcePath: string, bytes: Buffer): FileScanResult {
const sample = bytes.subarray(0, Math.min(bytes.length, 1024 * 1024)).toString("latin1");
if (sample.includes("EICAR-STANDARD-ANTIVIRUS-TEST-FILE")) {
throw new Error("File scan blocked a known malware signature.");
const textSample = sampleText(bytes);
const labels = new Set(scanLabels(sourcePath, bytes.length));
let blocked = false;
if (bytes.subarray(0, Math.min(bytes.length, 1024 * 1024)).toString("latin1").includes("EICAR-STANDARD-ANTIVIRUS-TEST-FILE")) {
labels.add("av:eicar");
blocked = true;
}
for (const scan of TEXT_SCAN_PATTERNS) {
if (!scan.pattern.test(textSample)) continue;
labels.add(scan.label);
if (scan.block) blocked = true;
}
if (containsPaymentCard(textSample)) {
labels.add("dlp:payment-card");
blocked = true;
}
labels.add(blocked ? "classification:restricted" : labels.has("dlp:email") ? "classification:confidential" : "classification:internal");
return {
state: FILE_SCAN_STATE.APPROVED,
labels: scanLabels(sourcePath, bytes.length),
state: blocked ? FILE_SCAN_STATE.BLOCKED : FILE_SCAN_STATE.APPROVED,
labels: [...labels].sort(),
scannedAt: new Date().toISOString(),
};
}
@@ -158,3 +187,28 @@ function scanLabels(sourcePath: string, sizeBytes: number): string[] {
if (sizeBytes > 100 * 1024 * 1024) labels.add("large");
return [...labels].sort();
}
function sampleText(bytes: Buffer): string {
return bytes.subarray(0, Math.min(bytes.length, 1024 * 1024)).toString("utf8");
}
function containsPaymentCard(text: string): boolean {
const matches = text.match(/\b(?:\d[ -]?){13,19}\b/g) ?? [];
return matches.some((match) => luhnValid(match.replace(/[ -]/g, "")));
}
function luhnValid(value: string): boolean {
if (!/^\d{13,19}$/.test(value)) return false;
let sum = 0;
let doubleDigit = false;
for (let index = value.length - 1; index >= 0; index -= 1) {
let digit = Number(value[index]);
if (doubleDigit) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
doubleDigit = !doubleDigit;
}
return sum % 10 === 0;
}
@@ -305,7 +305,12 @@ function FileRow({
>
<div className="row" style={{ gap: 10, minWidth: 0 }}>
<FileIcon kind={file.kind} />
<span style={{ fontWeight: 480, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{file.name}</span>
<div style={{ display: "grid", gap: 3, minWidth: 0 }}>
<span style={{ fontWeight: 480, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{file.name}</span>
<span className="mono" style={{ color: "var(--text-3)", fontSize: 10, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
sha:{shortChecksum(file.sha256)} · obj:{shortObjectRef(file.storagePath)}
</span>
</div>
{file.pinned && <Pill tone="violet" dot>Pinned</Pill>}
</div>
<div className="num mono" style={{ color: "var(--text-3)", fontSize: 12 }}>{formatBytes(file.sizeBytes)}</div>
@@ -340,7 +345,7 @@ function PinnedCard({
<div
style={{
border: "1px solid var(--border)",
borderRadius: 10,
borderRadius: 8,
background: "var(--panel)",
padding: 14,
display: "flex",
@@ -363,10 +368,14 @@ function PinnedCard({
<div style={{ fontSize: 13, fontWeight: 540, letterSpacing: "-0.005em", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{file.name}
</div>
<div className="mono" style={{ color: "var(--text-3)", fontSize: 10, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
sha:{shortChecksum(file.sha256)} · obj:{shortObjectRef(file.storagePath)}
</div>
<div className="row" style={{ gap: 6 }}>
<Pill tone={file.scanState === "approved" ? "green" : "red"}>{file.scanState === "approved" ? "Clean" : "Blocked"}</Pill>
<span className="mono" style={{ color: "var(--text-3)", fontSize: 11 }}>{grantCount}</span>
</div>
<ScanLabelStrip file={file} />
<div className="row" style={{ gap: 8, marginTop: "auto", minWidth: 0 }}>
<Avatar name={ownerName} size="sm" bot={isAgentOwner(file)} />
<span style={{ fontSize: 11, color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
@@ -377,6 +386,18 @@ function PinnedCard({
);
}
function ScanLabelStrip({ file }: { file: FileObjectRecord }) {
const labels = parseScanLabels(file).slice(0, 2);
if (labels.length === 0) return null;
return (
<div className="row" style={{ gap: 5, minWidth: 0, flexWrap: "wrap" }}>
{labels.map((label) => (
<Pill key={label}>{label}</Pill>
))}
</div>
);
}
function FileIcon({ kind, big = false }: { kind: string; big?: boolean }) {
const map = FILE_COLOR_MAP[kind] ?? FILE_COLOR_MAP.doc;
if (!map) throw new Error("File color map default missing.");
@@ -443,6 +464,25 @@ function fileGrantCount(fileId: string, grants: FileAccessGrantRecord[]): number
return grants.filter((grant) => grant.fileId === fileId).length;
}
function parseScanLabels(file: FileObjectRecord): string[] {
const parsed: unknown = JSON.parse(file.scanLabelsJson);
if (!Array.isArray(parsed) || parsed.some((label) => typeof label !== "string")) {
throw new Error("File scan labels are invalid.");
}
return parsed;
}
function shortChecksum(value: string): string {
if (!/^[a-f0-9]{8,}$/i.test(value)) throw new Error("File checksum is invalid.");
return value.slice(0, 8);
}
function shortObjectRef(value: string): string {
const parts = value.split(/[\\/]/).filter(Boolean);
if (parts.length === 0) throw new Error("File object reference is invalid.");
return parts.slice(-2).join("/");
}
function isRecent(iso: string): boolean {
const sevenDays = 7 * 24 * 60 * 60 * 1000;
return Date.now() - new Date(iso).getTime() <= sevenDays;
@@ -1,10 +1,11 @@
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { randomUUID } from "node:crypto";
import { createHash, randomUUID } from "node:crypto";
import { DatabaseSync } from "node:sqlite";
import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { FileService } from "../../src/main/services/file-service";
import { OrganizationService } from "../../src/main/services/organization-service";
import { SnapshotService } from "../../src/main/services/snapshot-service";
import { SkillService } from "../../src/main/services/skill-service";
@@ -396,6 +397,65 @@ describe("domain services", () => {
connection.close();
});
test("imports files with checksum, scan labels, object refs, and workspace grants", () => {
const connection = createConnection();
const organization = new OrganizationService(connection.db);
const snapshot = new SnapshotService(connection.db);
const artifactRoot = join(tmpdir(), `agentcompany-file-artifacts-${randomUUID()}`);
const fileService = new FileService(connection.db, artifactRoot);
const sourceDir = join(tmpdir(), `agentcompany-file-sources-${randomUUID()}`);
mkdirSync(sourceDir, { recursive: true });
const cleanBody = "Release checklist for dana@example.com\n";
const secretBody = "-----BEGIN OPENSSH PRIVATE KEY-----\nabc123\n-----END OPENSSH PRIVATE KEY-----\n";
const cleanPath = join(sourceDir, "release-notes.md");
const secretPath = join(sourceDir, "deploy.env");
writeFileSync(cleanPath, cleanBody);
writeFileSync(secretPath, secretBody);
let orgId = "";
connection.transaction(() => {
orgId = organization.createOrganization({
organizationName: "Acme Operations",
employeeName: "Dana Lee",
mission: "Coordinate agents with auditable approvals.",
goal: "Ship a verified control plane.",
});
});
const cleanFileId = fileService.importPath(orgId, cleanPath);
const blockedFileId = fileService.importPath(orgId, secretPath);
const state = snapshot.load();
const cleanFile = state.files.find((file) => file.id === cleanFileId);
const blockedFile = state.files.find((file) => file.id === blockedFileId);
if (!cleanFile || !blockedFile) throw new Error("Imported files were not recorded.");
expect(cleanFile.sha256).toBe(createHash("sha256").update(cleanBody).digest("hex"));
expect(cleanFile.storagePath).toContain(cleanFileId);
expect(cleanFile.scanState).toBe("approved");
expect(JSON.parse(cleanFile.scanLabelsJson)).toEqual(expect.arrayContaining(["classification:confidential", "dlp:email", "kind:md"]));
expect(blockedFile.scanState).toBe("blocked");
expect(JSON.parse(blockedFile.scanLabelsJson)).toEqual(expect.arrayContaining(["classification:restricted", "dlp:private-key", "kind:env"]));
expect(state.fileAccessGrants).toContainEqual(expect.objectContaining({
fileId: cleanFileId,
grantedToType: "org_group",
grantedToId: `${orgId}:workspace`,
accessLevel: "manage",
}));
expect(state.fileAccessGrants).toContainEqual(expect.objectContaining({
fileId: blockedFileId,
grantedToType: "org_group",
grantedToId: `${orgId}:workspace`,
accessLevel: "manage",
}));
const importAudit = state.auditEvents.find((event) => event.entityId === blockedFileId && event.action === "imported");
if (!importAudit) throw new Error("File import audit event was not recorded.");
expect(importAudit.payloadJson).toContain("\"scanState\":\"blocked\"");
expect(importAudit.payloadJson).toContain("dlp:private-key");
expect(importAudit.payloadJson).toContain("objectRef");
rmSync(sourceDir, { recursive: true, force: true });
rmSync(artifactRoot, { recursive: true, force: true });
connection.close();
});
test("creates approval and waiting presence from chat escalation", () => {
const connection = createConnection();
const organization = new OrganizationService(connection.db);