From 7c978c6a10180ac63f46789ab6317639563bfe49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 23 May 2026 23:16:40 -0400 Subject: [PATCH] Strengthen file scan evidence --- .../src/main/services/file-service.ts | 70 ++++++++++++++++--- .../src/renderer/src/pages/FilesPage.tsx | 44 +++++++++++- .../tests/unit/domain-services.test.ts | 62 +++++++++++++++- 3 files changed, 165 insertions(+), 11 deletions(-) diff --git a/AgentCompany-New/src/main/services/file-service.ts b/AgentCompany-New/src/main/services/file-service.ts index b8c6edc7..138a764f 100644 --- a/AgentCompany-New/src/main/services/file-service.ts +++ b/AgentCompany-New/src/main/services/file-service.ts @@ -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; +} diff --git a/AgentCompany-New/src/renderer/src/pages/FilesPage.tsx b/AgentCompany-New/src/renderer/src/pages/FilesPage.tsx index d6d7123a..baa35da8 100644 --- a/AgentCompany-New/src/renderer/src/pages/FilesPage.tsx +++ b/AgentCompany-New/src/renderer/src/pages/FilesPage.tsx @@ -305,7 +305,12 @@ function FileRow({ >
- {file.name} +
+ {file.name} + + sha:{shortChecksum(file.sha256)} · obj:{shortObjectRef(file.storagePath)} + +
{file.pinned && Pinned}
{formatBytes(file.sizeBytes)}
@@ -340,7 +345,7 @@ function PinnedCard({
{file.name}
+
+ sha:{shortChecksum(file.sha256)} · obj:{shortObjectRef(file.storagePath)} +
{file.scanState === "approved" ? "Clean" : "Blocked"} {grantCount}
+
@@ -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 ( +
+ {labels.map((label) => ( + {label} + ))} +
+ ); +} + 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; diff --git a/AgentCompany-New/tests/unit/domain-services.test.ts b/AgentCompany-New/tests/unit/domain-services.test.ts index c7fc6625..8dc9c098 100644 --- a/AgentCompany-New/tests/unit/domain-services.test.ts +++ b/AgentCompany-New/tests/unit/domain-services.test.ts @@ -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);