Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/server/evidence/ingestion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const EVIDENCE_SOURCES = [
"patch-verification",
"patch-remediation",
"stage-handoff",
"passive-recon",
] as const;

export type EvidenceSource = (typeof EVIDENCE_SOURCES)[number];
Expand Down
7 changes: 7 additions & 0 deletions src/server/recon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@ export {
type NormalizedDiscoveryUrl,
normalizeDiscoveryArtifacts,
} from "./discovery-artifact-normalizer";
export {
buildPassiveAuthSurfaceSummary,
type PassiveAuthSurfaceSummary,
type PersistPassiveAuthSurfaceInput,
type PersistPassiveAuthSurfaceResult,
persistPassiveAuthSurface,
} from "./passive-auth-surface";
177 changes: 177 additions & 0 deletions src/server/recon/passive-auth-surface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import type {
ArtifactServiceInstance,
CreateArtifactResult,
} from "../evidence";
import {
type AuthSurfaceCategory,
type DiscoveryArtifactInput,
type DiscoveryBlockerReason,
type NormalizedDiscovery,
normalizeDiscoveryArtifacts,
} from "./discovery-artifact-normalizer";

export type PassiveAuthSurfaceSummary = {
targetId: string;
rawArtifactIds: string[];
authRoutes: Array<{
category: AuthSurfaceCategory;
confidence: "high" | "medium";
urls: string[];
evidenceArtifactIds: string[];
}>;
blockers: Array<{
reason: DiscoveryBlockerReason;
evidenceArtifactIds: string[];
evidence: string;
}>;
unknowns: string[];
nextSteps: {
passive: string[];
approvalGated: string[];
reportOrPatch: string[];
};
};

export type PersistPassiveAuthSurfaceInput = {
projectId: string;
threadId?: string;
targetId: string;
taskId?: string;
artifacts: readonly DiscoveryArtifactInput[];
};

export type PersistPassiveAuthSurfaceResult = {
normalized: NormalizedDiscovery;
summary: PassiveAuthSurfaceSummary;
artifact: CreateArtifactResult;
};

type ArtifactWriter = Pick<ArtifactServiceInstance, "createArtifact">;

export async function persistPassiveAuthSurface(
input: PersistPassiveAuthSurfaceInput,
artifactWriter: ArtifactWriter,
): Promise<PersistPassiveAuthSurfaceResult> {
const normalized = normalizeDiscoveryArtifacts(input.artifacts);
const summary = buildPassiveAuthSurfaceSummary(input.targetId, normalized);
const content = JSON.stringify(summary, null, 2);
const artifact = await artifactWriter.createArtifact({
projectId: input.projectId,
...(input.threadId ? { threadId: input.threadId } : {}),
targetId: input.targetId,
...(input.taskId ? { taskId: input.taskId } : {}),
name: `passive-auth-surface-${input.targetId}.json`,
kind: "report",
contentType: "application/json",
content,
indexText: content,
source: "passive-recon",
indexForRag: true,
agentGenerated: true,
metadata: {
workflow: "passive-auth-surface-v1",
rawArtifactIds: summary.rawArtifactIds,
blockerReasons: summary.blockers.map((blocker) => blocker.reason),
authCategories: summary.authRoutes.map((route) => route.category),
},
});

return { normalized, summary, artifact };
}

export function buildPassiveAuthSurfaceSummary(
targetId: string,
normalized: NormalizedDiscovery,
): PassiveAuthSurfaceSummary {
const authRoutes = new Map<
AuthSurfaceCategory,
{
confidence: "high" | "medium";
urls: Set<string>;
evidenceArtifactIds: Set<string>;
}
>();

for (const candidate of normalized.authCandidates) {
for (const category of candidate.categories) {
const group = authRoutes.get(category) ?? {
confidence: "medium" as const,
urls: new Set<string>(),
evidenceArtifactIds: new Set<string>(),
};
if (candidate.confidence === "high") group.confidence = "high";
group.urls.add(candidate.url);
for (const artifactId of candidate.sourceArtifactIds) {
group.evidenceArtifactIds.add(artifactId);
}
authRoutes.set(category, group);
}
}

const groupedRoutes = [...authRoutes.entries()]
.map(([category, group]) => ({
category,
confidence: group.confidence,
urls: [...group.urls].sort(),
evidenceArtifactIds: [...group.evidenceArtifactIds].sort(),
}))
.sort((left, right) => left.category.localeCompare(right.category));
const blockerReasons = new Set(
normalized.blockerSignals.map((blocker) => blocker.reason),
);

return {
targetId,
rawArtifactIds: [...normalized.rawArtifactIds],
authRoutes: groupedRoutes,
blockers: normalized.blockerSignals.map((blocker) => ({
reason: blocker.reason,
evidenceArtifactIds: [...blocker.sourceArtifactIds],
evidence: blocker.evidence,
})),
unknowns: buildUnknowns(
new Set(groupedRoutes.map((route) => route.category)),
),
nextSteps: {
passive: [
"Review retrieved reference material and existing project evidence for the mapped routes.",
"Resolve remaining auth-flow unknowns from supplied source, manifests, and historical artifacts.",
],
approvalGated: blockerReasons.size
? [
"Resolve recorded blockers before proposing any active validation.",
"Request a target-bound approval for the narrowest reproducible probe only if passive evidence is insufficient.",
]
: [
"Request a target-bound approval before any live request, browser interaction, or credential test.",
],
reportOrPatch: [
"Keep mapped routes as evidence-backed hypotheses until validation establishes impact.",
],
},
};
}

function buildUnknowns(categories: ReadonlySet<AuthSurfaceCategory>): string[] {
const unknowns: string[] = [];
if (
!categories.has("login") &&
!categories.has("sso") &&
!categories.has("oauth")
) {
unknowns.push(
"Primary authentication entry point is not established by current evidence.",
);
}
if (!categories.has("session") && !categories.has("token")) {
unknowns.push(
"Session or token lifecycle is not established by current evidence.",
);
}
if (!categories.has("password-recovery")) {
unknowns.push(
"Account recovery surface is not established by current evidence.",
);
}
return unknowns;
}
65 changes: 63 additions & 2 deletions tests/integration/discovery-artifact-normalizer.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";

import { normalizeDiscoveryArtifacts } from "../../src/server/recon";
import {
normalizeDiscoveryArtifacts,
persistPassiveAuthSurface,
} from "../../src/server/recon";

describe("discovery artifact normalization", () => {
it("turns mixed passive evidence into deduplicated, attributable auth-surface signals", () => {
Expand Down Expand Up @@ -77,4 +80,62 @@ describe("discovery artifact normalization", () => {
]);
expect(result.observations.token).toEqual(["www-authenticate: Bearer"]);
});

it("persists a redaction-ready, RAG-indexed summary linked to its raw evidence", async () => {
const createArtifact = vi.fn(async () => ({
id: "artifact-summary",
projectId: "project-1",
threadId: "thread-1",
name: "passive-auth-surface-target-1.json",
kind: "report",
indexing: { status: "indexed" as const, chunkCount: 1 },
}));

const result = await persistPassiveAuthSurface(
{
projectId: "project-1",
threadId: "thread-1",
targetId: "target-1",
taskId: "task-1",
artifacts: [
{
artifactId: "artifact-raw",
source: "upload",
content: [
"https://app.example.test/login",
"HTTP 429 Too Many Requests",
].join("\n"),
},
],
},
{ createArtifact } as never,
);

expect(result.summary.authRoutes).toEqual([
{
category: "login",
confidence: "high",
urls: ["https://app.example.test/login"],
evidenceArtifactIds: ["artifact-raw"],
},
]);
expect(result.summary.blockers[0]).toMatchObject({
reason: "rate-limited",
evidenceArtifactIds: ["artifact-raw"],
});
expect(createArtifact).toHaveBeenCalledWith(
expect.objectContaining({
projectId: "project-1",
threadId: "thread-1",
targetId: "target-1",
taskId: "task-1",
source: "passive-recon",
indexForRag: true,
metadata: expect.objectContaining({
rawArtifactIds: ["artifact-raw"],
blockerReasons: ["rate-limited"],
}),
}),
);
});
});