From ae032cf3be38b9e5258337aa71ec727ee9899d95 Mon Sep 17 00:00:00 2001 From: initstring <26131150+initstring@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:37:59 +1000 Subject: [PATCH 1/4] Expand taxonomy targets --- docs/dev/DESIGN.md | 12 +- docs/getting-started.md | 6 +- .../20250306200000_targets/migration.sql | 25 ++ prisma/schema.prisma | 22 +- scripts/demo-data.ts | 52 +++-- .../(protected-routes)/settings/data/page.tsx | 2 +- .../settings/taxonomy/page.tsx | 4 +- .../scorecard/threat-coverage-section.tsx | 12 +- .../create-operation-modal.test.tsx | 4 +- .../components/create-operation-modal.tsx | 24 +- .../operation-detail-page/index.tsx | 9 +- .../operations/components/operations-page.tsx | 7 +- .../technique-editor/execution-section.tsx | 28 +-- .../components/technique-editor/index.tsx | 13 +- .../technique-editor/taxonomy-selector.tsx | 29 ++- .../hooks/useTechniqueEditorForm.ts | 19 +- .../settings/components/crown-jewels-tab.tsx | 189 --------------- .../settings/components/targets-tab.tsx | 217 ++++++++++++++++++ src/lib/exportOperationReport.ts | 8 +- src/server/api/routers/data/index.ts | 31 +-- src/server/api/routers/operations.ts | 8 +- .../api/routers/taxonomy/crown-jewels.ts | 55 ----- src/server/api/routers/taxonomy/index.ts | 4 +- src/server/api/routers/taxonomy/targets.ts | 64 ++++++ src/server/services/operationService.ts | 34 +-- src/server/services/taxonomyService.ts | 24 +- src/server/services/usageService.ts | 4 +- src/test/data-restore.test.ts | 6 +- src/test/data.test.ts | 10 +- src/test/factories/taxonomy.ts | 7 +- src/test/operation-service.test.ts | 10 +- src/test/operations-access.test.ts | 2 +- .../operations-create-update-delete.test.ts | 8 +- src/test/operations-read.test.ts | 2 +- src/test/server/services/usageService.test.ts | 8 +- src/test/taxonomy-service.test.ts | 14 +- src/test/taxonomy.test.ts | 23 +- 37 files changed, 546 insertions(+), 450 deletions(-) create mode 100644 prisma/migrations/20250306200000_targets/migration.sql delete mode 100644 src/features/settings/components/crown-jewels-tab.tsx create mode 100644 src/features/settings/components/targets-tab.tsx delete mode 100644 src/server/api/routers/taxonomy/crown-jewels.ts create mode 100644 src/server/api/routers/taxonomy/targets.ts diff --git a/docs/dev/DESIGN.md b/docs/dev/DESIGN.md index bd3d243..b3091cb 100644 --- a/docs/dev/DESIGN.md +++ b/docs/dev/DESIGN.md @@ -39,11 +39,11 @@ Plan and execute red‑team operations and measure defensive effectiveness (dete ## Core Entities -- Operation { name, description, tags[], crownJewels[], threatActor?, status, visibility, accessGroups[], techniques[] } +- Operation { name, description, tags[], targets[], threatActor?, status, visibility, accessGroups[], techniques[] } - Technique { tactic, technique, subTechnique?, description, start/end, sourceIp?, targetSystem?, crownJewelTargeted?, crownJewelCompromised?, tools[] } - Outcome { type: DETECTION | PREVENTION | ATTRIBUTION, status, tools[]/logSources[], timestamp? } - ThreatActor { name, description, topThreat, mitreTechniques[] } -- CrownJewel { name, description } +- Target { name, description, isCrownJewel } - Tool { name, type: DEFENSIVE | OFFENSIVE, category } - ToolCategory { name, type } - Tag { name, description, color } @@ -61,16 +61,16 @@ Plan and execute red‑team operations and measure defensive effectiveness (dete - Filters: search, status (All/Planning/Active/Completed/Cancelled), selectable tag chips. - List: neutral operation cards; click to open detail. Card delete uses ConfirmModal. -- Create/Edit Operation: elevated modal with name/description, optional threat actor, dates, tags, crown jewels. +- Create/Edit Operation: elevated modal with name/description, optional threat actor, dates, tags, targets (mark crown jewels). #### Operation Detail -- Header: name, description, tags, threat actor, crown jewels, status. +- Header: name, description, tags, threat actor, targets (CJ flagged), status. - KPIs: detection/prevention/attribution (%) computed from graded outcomes (excludes N/A). - Tabs - Techniques: drag‑and‑drop list; InlineActions for edit/delete. Technique Editor (elevated) with: - Overview: tactic/technique (sub‑tech aware) + description. - - Execution: start/end (datetime with “Now”), source IP, target system, offensive tools, crown‑jewel flags. + - Execution: start/end (datetime with “Now”), source IP, target system, offensive tools, target selection + crown jewel flag. - Outcomes: grade detection/prevention/attribution; add tools/log sources; optional timestamps. - ATT&CK Heatmap: full MITRE matrix with executed highlighting; sub‑tech expansion; ops/all toggle available in analytics view. - Attack Flow: simple flow of techniques (editors can organize). @@ -90,7 +90,7 @@ Unified pattern across tabs: SettingsHeader + EntityListCard + EntityModal; Inli - Users: create/edit; role picker; delete via ConfirmModal. - Groups: create/edit; manage membership; one Tag per Group; delete via ConfirmModal. -- Taxonomy: Tags, Tool Categories (by type), Tools, Threat Actors (attach ATT&CK techniques), Crown Jewels, Log Sources. +- Taxonomy: Tags, Tool Categories (by type), Tools, Threat Actors (attach ATT&CK techniques), Targets (with crown jewel toggle), Log Sources. - Data: overview metrics; export/import a combined operations + taxonomy backup (always replaces existing data); clear-all confirmation. ## Data & Validation diff --git a/docs/getting-started.md b/docs/getting-started.md index 1fb11ce..1c94cc9 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -11,7 +11,7 @@ This guide walks through the first workflow after you launch the application. Fo - Log Sources. Examples: Sysmon, Windows Event Logs, etc. These are used for recording attribution outcomes. - Optional: - Threat Actors. You can enter them manually or use the import option to pull in techniques directly from MITRE ATT&CK. - - Crown Jewels. Examples: Production DB, Source Code Repo, etc. + - Targets. Examples: Production DB, Source Code Repo, employee credentials, etc. Mark the ones that are crown jewels so analytics continue to highlight them. - Tags. Examples: Stealth, Purple Team, etc. These can be applied to operations and used for filtering lists analytics.

@@ -38,7 +38,7 @@ This guide walks through the first workflow after you launch the application. Fo - From MITRE campaign: imports the techniques from a MITRE ATT&CK campaign - Provide the name, description, status - Set the start and end dates. These dates drive analytics such as trends and duration metrics, so keep them accurate. -- Optionally, configure tags, crown jewels, threat actor being emulated, and group access restrictions. +- Optionally, configure tags, targets (with crown jewel markers), threat actor being emulated, and group access restrictions.

@@ -53,7 +53,7 @@ This guide walks through the first workflow after you launch the application. Fo - Use the Tactic/Technique pickers to choose from the catalog - Fill in an optional description - Execution tab: - - Timing, execution details, offensive tooling, and crown jewel targeting + - Timing, execution details, offensive tooling, and target selection (with crown jewel access tracking) - Outcomes tab: - Was the technique detected, prevented, or attributed later during IR? - What tooling and log sources were involved in successful outcomes or SHOULD HAVE BEEN involved in failed outcomes? diff --git a/prisma/migrations/20250306200000_targets/migration.sql b/prisma/migrations/20250306200000_targets/migration.sql new file mode 100644 index 0000000..782009e --- /dev/null +++ b/prisma/migrations/20250306200000_targets/migration.sql @@ -0,0 +1,25 @@ +-- Rename CrownJewel table to Target and preserve existing data +ALTER TABLE "CrownJewel" RENAME TO "Target"; + +-- Add crown jewel flag (defaults to false for legacy records) +ALTER TABLE "Target" ADD COLUMN "isCrownJewel" BOOLEAN NOT NULL DEFAULT false; + +-- Drop legacy indexes before recreating them with the new names +DROP INDEX IF EXISTS "CrownJewel_name_key"; +DROP INDEX IF EXISTS "CrownJewel_name_idx"; + +-- Recreate indexes with the expected Target names +CREATE UNIQUE INDEX IF NOT EXISTS "Target_name_key" ON "Target"("name"); +CREATE INDEX IF NOT EXISTS "Target_name_idx" ON "Target"("name"); +CREATE INDEX IF NOT EXISTS "Target_isCrownJewel_idx" ON "Target"("isCrownJewel"); + +-- Rename the join table that connects operations and targets +ALTER TABLE "_OperationCrownJewels" RENAME TO "_OperationTargets"; + +-- Drop the legacy join table indexes before recreating them +DROP INDEX IF EXISTS "_OperationCrownJewels_AB_unique"; +DROP INDEX IF EXISTS "_OperationCrownJewels_B_index"; + +-- Recreate join table indexes under the new name +CREATE UNIQUE INDEX IF NOT EXISTS "_OperationTargets_AB_unique" ON "_OperationTargets"("A", "B"); +CREATE INDEX IF NOT EXISTS "_OperationTargets_B_index" ON "_OperationTargets"("B"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6c77015..287381e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -145,15 +145,17 @@ model ThreatActor { @@index([topThreat]) } -model CrownJewel { - id String @id @default(cuid()) - name String @unique // e.g., "Customer Database", "Source Code Repository" - description String - operations Operation[] @relation("OperationCrownJewels") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt +model Target { + id String @id @default(cuid()) + name String @unique // e.g., "Customer Database", "Source Code Repository" + description String + isCrownJewel Boolean @default(false) + operations Operation[] @relation("OperationTargets") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@index([name]) + @@index([isCrownJewel]) } model Tag { @@ -266,9 +268,9 @@ model Operation { threatActorId String? // Many-to-many relationships - tags Tag[] @relation("OperationTags") - crownJewels CrownJewel[] @relation("OperationCrownJewels") - techniques Technique[] + tags Tag[] @relation("OperationTags") + targets Target[] @relation("OperationTargets") + techniques Technique[] // Access control visibility OperationVisibility @default(EVERYONE) diff --git a/scripts/demo-data.ts b/scripts/demo-data.ts index bc9431d..998c5d0 100644 --- a/scripts/demo-data.ts +++ b/scripts/demo-data.ts @@ -76,20 +76,24 @@ async function seedTaxonomy() { logSources[name] = ls.id; } - const crownJewels = {} as Record; - for (const cj of [ - "Customer Database", - "Source Code Repository", - "Payment Processing System", - "Email Server", - "HR Records", - ]) { - const c = await db.crownJewel.upsert({ - where: { name: cj }, - update: {}, - create: { name: cj, description: `${cj} crown jewel` }, + const targets = {} as Record; + const targetDefs: Array<{ name: string; description: string; isCrownJewel: boolean }> = [ + { name: "Customer Database", description: "Primary production customer data store", isCrownJewel: true }, + { name: "Source Code Repository", description: "Core application source control", isCrownJewel: true }, + { name: "Payment Processing System", description: "Systems handling card payments", isCrownJewel: true }, + { name: "Email Server", description: "Corporate email infrastructure", isCrownJewel: true }, + { name: "HR Records", description: "Sensitive employee data store", isCrownJewel: true }, + { name: "Employee Credentials", description: "Employee identity and password vault", isCrownJewel: false }, + { name: "Employee Endpoint", description: "Standard employee workstation image", isCrownJewel: false }, + { name: "Application Access Token", description: "Service-to-service OAuth token", isCrownJewel: false }, + ]; + for (const target of targetDefs) { + const record = await db.target.upsert({ + where: { name: target.name }, + update: { description: target.description, isCrownJewel: target.isCrownJewel }, + create: target, }); - crownJewels[cj] = c.id; + targets[target.name] = record.id; } const tags = {} as Record; @@ -170,14 +174,14 @@ async function seedTaxonomy() { threatActors[a.name] = { id: actor.id }; } - return { tools, logSources, crownJewels, threatActors, tags }; + return { tools, logSources, targets, threatActors, tags }; } interface SeedCtx { userId: string; tools: Record; logSources: Record; - crownJewels: Record; + targets: Record; threatActors: Record; tags: Record; } @@ -213,13 +217,13 @@ interface OperationSeed { threatActor?: string; start: Date; end: Date | null; - crownJewels: string[]; + targets: string[]; tags: string[]; techniques: TechniqueSeed[]; } async function seedOperations(ctx: SeedCtx) { - const { userId, tools, logSources, crownJewels, threatActors, tags } = ctx; + const { userId, tools, logSources, targets, threatActors, tags } = ctx; const operations: OperationSeed[] = [ { @@ -229,7 +233,7 @@ async function seedOperations(ctx: SeedCtx) { threatActor: threatActors.APT29!.id, start: new Date("2025-01-05T08:00:00Z"), end: new Date("2025-02-15T16:00:00Z"), - crownJewels: ["Email Server", "Payment Processing System"], + targets: ["Email Server", "Payment Processing System", "Employee Credentials"], tags: ["Stealth"], techniques: [ { @@ -348,7 +352,7 @@ async function seedOperations(ctx: SeedCtx) { threatActor: threatActors.FIN7!.id, start: new Date("2025-02-15T10:00:00Z"), end: new Date("2025-05-15T18:00:00Z"), - crownJewels: ["Payment Processing System"], + targets: ["Payment Processing System", "Employee Endpoint"], tags: ["Opportunistic"], techniques: [ { @@ -467,7 +471,7 @@ async function seedOperations(ctx: SeedCtx) { threatActor: threatActors["Lazarus Group"]!.id, start: new Date("2024-08-01T09:00:00Z"), end: new Date("2025-01-31T17:00:00Z"), - crownJewels: ["Source Code Repository"], + targets: ["Source Code Repository", "Application Access Token"], tags: ["Purple Team"], techniques: [ { @@ -576,7 +580,7 @@ async function seedOperations(ctx: SeedCtx) { status: OperationStatus.COMPLETED, start: new Date("2025-04-01T08:00:00Z"), end: new Date("2025-07-31T18:00:00Z"), - crownJewels: ["Customer Database", "HR Records"], + targets: ["Customer Database", "HR Records", "Employee Credentials"], tags: ["Stealth"], techniques: [ { @@ -781,7 +785,7 @@ async function seedOperations(ctx: SeedCtx) { status: OperationStatus.PLANNING, start: new Date("2025-06-01T00:00:00Z"), end: null, - crownJewels: ["Source Code Repository"], + targets: ["Source Code Repository", "Employee Credentials"], tags: ["Purple Team"], techniques: [ { @@ -832,8 +836,8 @@ async function seedOperations(ctx: SeedCtx) { endDate: op.end, createdById: userId, threatActorId: op.threatActor ?? undefined, - crownJewels: { - connect: op.crownJewels.map((n) => ({ id: crownJewels[n] })), + targets: { + connect: op.targets.map((n) => ({ id: targets[n] })), }, tags: { connect: op.tags.map((n) => ({ id: tags[n] })), diff --git a/src/app/(protected-routes)/settings/data/page.tsx b/src/app/(protected-routes)/settings/data/page.tsx index 310d801..8d976bd 100644 --- a/src/app/(protected-routes)/settings/data/page.tsx +++ b/src/app/(protected-routes)/settings/data/page.tsx @@ -142,7 +142,7 @@ export default function DataSettingsPage() { { label: "Techniques", value: stats.techniques }, { label: "Outcomes", value: stats.outcomes }, { label: "Threat Actors", value: stats.threatActors }, - { label: "Crown Jewels", value: stats.crownJewels }, + { label: "Targets", value: stats.targets }, { label: "Tags", value: stats.tags }, { label: "Tools", value: stats.tools }, { label: "Log Sources", value: stats.logSources }, diff --git a/src/app/(protected-routes)/settings/taxonomy/page.tsx b/src/app/(protected-routes)/settings/taxonomy/page.tsx index 72efa74..f5621ed 100644 --- a/src/app/(protected-routes)/settings/taxonomy/page.tsx +++ b/src/app/(protected-routes)/settings/taxonomy/page.tsx @@ -1,5 +1,5 @@ import ThreatActorsTab from "@features/settings/components/threat-actors-tab"; -import CrownJewelsTab from "@features/settings/components/crown-jewels-tab"; +import TargetsTab from "@features/settings/components/targets-tab"; import TagsTab from "@features/settings/components/tags-tab"; import ToolCategoriesTab from "@features/settings/components/tool-categories-tab"; import ToolsTab from "@features/settings/components/tools-tab"; @@ -15,7 +15,7 @@ export default function TaxonomyPage() { - + diff --git a/src/features/analytics/components/scorecard/threat-coverage-section.tsx b/src/features/analytics/components/scorecard/threat-coverage-section.tsx index c2b5949..c4763e7 100644 --- a/src/features/analytics/components/scorecard/threat-coverage-section.tsx +++ b/src/features/analytics/components/scorecard/threat-coverage-section.tsx @@ -40,7 +40,11 @@ export function ThreatCoverageSection({ start, end, tagIds }: ThreatCoverageSect } }, [fetchNextPage, hasNextPage, isFetchingNextPage]); const { data: threatActors } = api.taxonomy.threatActors.list.useQuery(); - const { data: crownJewels } = api.taxonomy.crownJewels.list.useQuery(); + const { data: targetsData, isLoading: isTargetsLoading } = api.taxonomy.targets.list.useQuery(); + const crownJewels = useMemo( + () => (targetsData ?? []).filter((target) => target.isCrownJewel), + [targetsData], + ); const operations = useMemo(() => { const pages = operationsPages?.pages ?? []; @@ -130,7 +134,7 @@ export function ThreatCoverageSection({ start, end, tagIds }: ThreatCoverageSect if (!crownJewels) return []; return crownJewels.map((jewel) => { const targetingOps = operations.filter((op) => - op.crownJewels?.some((cj) => cj.id === jewel.id) + op.targets?.some((target) => target.id === jewel.id) ); if (targetingOps.length === 0) { return { @@ -200,9 +204,9 @@ export function ThreatCoverageSection({ start, end, tagIds }: ThreatCoverageSect isOperationsLoading || isFetching || isFetchingNextPage || + isTargetsLoading || !operationsPages || - !threatActors || - !crownJewels; + !threatActors; if (isLoading) { return ( diff --git a/src/features/operations/components/create-operation-modal.test.tsx b/src/features/operations/components/create-operation-modal.test.tsx index 388810e..5d74d3e 100644 --- a/src/features/operations/components/create-operation-modal.test.tsx +++ b/src/features/operations/components/create-operation-modal.test.tsx @@ -17,7 +17,7 @@ vi.mock("@/trpc/react", () => { taxonomy: { threatActors: { list: { useQuery: () => ({ data: [] }) } }, tags: { list: { useQuery: () => ({ data: [] }) } }, - crownJewels: { list: { useQuery: () => ({ data: [] }) } }, + targets: { list: { useQuery: () => ({ data: [] }) } }, }, groups: { list: { useQuery: () => ({ data: [] }) } }, operations: { @@ -60,7 +60,7 @@ describe("CreateOperationModal status select", () => { visibility: "EVERYONE" as OperationVisibility, accessGroups: [], tags: [], - crownJewels: [], + targets: [], techniques: [], createdBy: { id: "user-1", name: "Alice", email: "alice@example.com" }, threatActor: null, diff --git a/src/features/operations/components/create-operation-modal.tsx b/src/features/operations/components/create-operation-modal.tsx index 4a73bd1..93e11c6 100644 --- a/src/features/operations/components/create-operation-modal.tsx +++ b/src/features/operations/components/create-operation-modal.tsx @@ -29,7 +29,7 @@ const OperationFormSchema = z startDate: z.string().optional(), endDate: z.string().optional(), tagIds: z.array(z.string()).optional(), - crownJewelIds: z.array(z.string()).optional(), + targetIds: z.array(z.string()).optional(), visibility: z.nativeEnum(OperationVisibility), accessGroupIds: z.array(z.string()).optional(), }) @@ -73,7 +73,7 @@ function buildDefaultValues(operation?: Operation): OperationFormValues { startDate: formatDateForInput(operation?.startDate), endDate: formatDateForInput(operation?.endDate), tagIds: operation?.tags.map((tag) => tag.id) ?? [], - crownJewelIds: operation?.crownJewels.map((cj) => cj.id) ?? [], + targetIds: operation?.targets.map((target) => target.id) ?? [], visibility: operation?.visibility ?? OperationVisibility.EVERYONE, accessGroupIds: operation?.accessGroups?.map(({ group }) => group.id) ?? [], }; @@ -108,7 +108,7 @@ export default function CreateOperationModal({ isOpen, onClose, onSuccess, opera const visibility = watch("visibility"); const selectedTagIds = watch("tagIds") ?? []; - const selectedCrownJewelIds = watch("crownJewelIds") ?? []; + const selectedTargetIds = watch("targetIds") ?? []; const selectedGroupIds = watch("accessGroupIds") ?? []; useEffect(() => { @@ -119,12 +119,12 @@ export default function CreateOperationModal({ isOpen, onClose, onSuccess, opera const { data: threatActorsData } = api.taxonomy.threatActors.list.useQuery(); const { data: tagsData } = api.taxonomy.tags.list.useQuery(); - const { data: crownJewelsData } = api.taxonomy.crownJewels.list.useQuery(); + const { data: targetsData } = api.taxonomy.targets.list.useQuery(); const { data: groupsData } = api.groups.list.useQuery(); const threatActors = threatActorsData ?? []; const tags = tagsData ?? []; - const crownJewels = crownJewelsData ?? []; + const targets = targetsData ?? []; const groups = groupsData ?? []; const utils = api.useUtils(); @@ -168,7 +168,7 @@ export default function CreateOperationModal({ isOpen, onClose, onSuccess, opera startDate: values.startDate ? new Date(values.startDate) : undefined, endDate: values.endDate ? new Date(values.endDate) : undefined, tagIds: values.tagIds ?? [], - crownJewelIds: values.crownJewelIds ?? [], + targetIds: values.targetIds ?? [], visibility: values.visibility, accessGroupIds: values.visibility === OperationVisibility.GROUPS_ONLY ? values.accessGroupIds ?? [] : [], }; @@ -373,12 +373,12 @@ export default function CreateOperationModal({ isOpen, onClose, onSuccess, opera {/* Crown Jewels Selection */} setValue("crownJewelIds", ids, { shouldDirty: true })} - label="Crown Jewels Targeting" - description="Select crown jewels this operation will target:" + variant="targets" + items={targets} + selectedIds={selectedTargetIds} + onSelectionChange={(ids) => setValue("targetIds", ids, { shouldDirty: true })} + label="Planned Targets" + description="Select assets this operation intends to target. Crown jewels are flagged with a CJ badge." searchable multiple /> diff --git a/src/features/operations/components/operation-detail-page/index.tsx b/src/features/operations/components/operation-detail-page/index.tsx index 755c10d..b19eb34 100644 --- a/src/features/operations/components/operation-detail-page/index.tsx +++ b/src/features/operations/components/operation-detail-page/index.tsx @@ -147,10 +147,11 @@ export default function OperationDetailPage({ operationId }: Props) { Targeting: - {operation.crownJewels.length > 0 - ? operation.crownJewels.map(cj => cj.name).join(', ') - : 'General Infrastructure' - } + {operation.targets.length > 0 + ? operation.targets + .map((target) => (target.isCrownJewel ? `${target.name} (CJ)` : target.name)) + .join(', ') + : 'General Infrastructure'} diff --git a/src/features/operations/components/operations-page.tsx b/src/features/operations/components/operations-page.tsx index 4aacae2..e78b915 100644 --- a/src/features/operations/components/operations-page.tsx +++ b/src/features/operations/components/operations-page.tsx @@ -223,11 +223,14 @@ export default function OperationsPage() { )} - {operation.crownJewels.length > 0 && ( + {operation.targets.length > 0 && (

- Targeting: {operation.crownJewels.map(jewel => jewel.name).join(", ")} + Targeting:{" "} + {operation.targets + .map((target) => (target.isCrownJewel ? `${target.name} (CJ)` : target.name)) + .join(", ")}
)} diff --git a/src/features/operations/components/technique-editor/execution-section.tsx b/src/features/operations/components/technique-editor/execution-section.tsx index 8c70e5a..0cdc5e3 100644 --- a/src/features/operations/components/technique-editor/execution-section.tsx +++ b/src/features/operations/components/technique-editor/execution-section.tsx @@ -27,10 +27,10 @@ export interface ExecutionSectionProps { selectedOffensiveToolIds: string[]; onOffensiveToolIdsChange: (ids: string[]) => void; - // Crown jewels - crownJewels: Array<{ id: string; name: string; description: string }> | undefined; - selectedCrownJewelIds: string[]; - onCrownJewelIdsChange: (ids: string[]) => void; + // Targets + targets: Array<{ id: string; name: string; description: string; isCrownJewel?: boolean }> | undefined; + selectedTargetIds: string[]; + onTargetIdsChange: (ids: string[]) => void; crownJewelAccess: string; // "yes" | "no" | "" onCrownJewelAccessChange: (value: string) => void; executionSuccess: string; // "yes" | "no" | "" @@ -50,9 +50,9 @@ export default function ExecutionSection(props: ExecutionSectionProps) { offensiveTools, selectedOffensiveToolIds, onOffensiveToolIdsChange, - crownJewels, - selectedCrownJewelIds, - onCrownJewelIdsChange, + targets, + selectedTargetIds, + onTargetIdsChange, crownJewelAccess, onCrownJewelAccessChange, executionSuccess, @@ -135,18 +135,18 @@ export default function ExecutionSection(props: ExecutionSectionProps) {
({ ...target }))} + selectedIds={selectedTargetIds} + onSelectionChange={onTargetIdsChange} + label="Targets" + description="Select assets this technique targeted. Crown jewels are flagged with a CJ badge." compactHeader searchable={false} multiple={true} /> - {selectedCrownJewelIds.length > 0 && ( + {selectedTargetIds.length > 0 && (
diff --git a/src/features/operations/components/technique-editor/index.tsx b/src/features/operations/components/technique-editor/index.tsx index cb09487..4ccc49f 100644 --- a/src/features/operations/components/technique-editor/index.tsx +++ b/src/features/operations/components/technique-editor/index.tsx @@ -96,7 +96,7 @@ export default function TechniqueEditorModal({ const formHook = useTechniqueEditorForm({ operationId, existingTechnique: isEditMode ? existingTechnique : undefined, - crownJewels: operation?.crownJewels, + targets: operation?.targets, onSuccess, onClose, }); @@ -385,9 +385,14 @@ export default function TechniqueEditorModal({ offensiveTools={offensiveTools.map(t => ({ id: t.id, name: t.name }))} selectedOffensiveToolIds={form.watch("offensiveToolIds")} onOffensiveToolIdsChange={(ids) => form.setValue("offensiveToolIds", ids, { shouldDirty: true })} - crownJewels={operation?.crownJewels?.map((cj) => ({ id: cj.id, name: cj.name, description: cj.description ?? "" }))} - selectedCrownJewelIds={form.watch("selectedCrownJewelIds")} - onCrownJewelIdsChange={(ids) => form.setValue("selectedCrownJewelIds", ids, { shouldDirty: true })} + targets={operation?.targets?.map((target) => ({ + id: target.id, + name: target.name, + description: target.description ?? "", + isCrownJewel: target.isCrownJewel, + }))} + selectedTargetIds={form.watch("selectedTargetIds")} + onTargetIdsChange={(ids) => form.setValue("selectedTargetIds", ids, { shouldDirty: true })} crownJewelAccess={cjAccess} onCrownJewelAccessChange={(value) => form.setValue("crownJewelAccess", value as "" | "yes" | "no", { shouldDirty: true })} executionSuccess={execSuccess} diff --git a/src/features/operations/components/technique-editor/taxonomy-selector.tsx b/src/features/operations/components/technique-editor/taxonomy-selector.tsx index f589d76..13e20a7 100644 --- a/src/features/operations/components/technique-editor/taxonomy-selector.tsx +++ b/src/features/operations/components/technique-editor/taxonomy-selector.tsx @@ -24,8 +24,9 @@ interface TagItem extends TaxonomyItem { color: string; } -interface CrownJewelItem extends TaxonomyItem { +interface TargetItem extends TaxonomyItem { description: string; + isCrownJewel?: boolean; } interface ThreatActorItem extends TaxonomyItem { @@ -59,8 +60,8 @@ interface TagSelectorProps extends BaseSelectorProps { variant: "tags"; } -interface CrownJewelSelectorProps extends BaseSelectorProps { - variant: "crown-jewels"; +interface TargetSelectorProps extends BaseSelectorProps { + variant: "targets"; } interface ThreatActorSelectorProps extends BaseSelectorProps { @@ -68,7 +69,7 @@ interface ThreatActorSelectorProps extends BaseSelectorProps { multiple?: false; // Threat actor is typically single selection } -type TaxonomySelectorProps = TagSelectorProps | CrownJewelSelectorProps | ThreatActorSelectorProps; +type TaxonomySelectorProps = TagSelectorProps | TargetSelectorProps | ThreatActorSelectorProps; interface ToolsSelectorProps extends BaseSelectorProps { variant: "tools"; } @@ -85,11 +86,11 @@ const selectorConfig = { emptyMessage: "No tags available", searchPlaceholder: "Search tags...", }, - "crown-jewels": { + targets: { icon: Shield, - title: "Crown Jewels", - emptyMessage: "No crown jewels available", - searchPlaceholder: "Search crown jewels...", + title: "Targets", + emptyMessage: "No targets available", + searchPlaceholder: "Search targets...", }, "threat-actors": { icon: Target, @@ -222,7 +223,7 @@ export default function TaxonomySelector(props: AllSelectorProps) { ); } - // Render for multiple selection (tags, crown jewels) + // Render for multiple selection (tags, targets) return (
{compactHeader ? ( @@ -302,9 +303,10 @@ export default function TaxonomySelector(props: AllSelectorProps) {
)} - {variant === "crown-jewels" && ( + {variant === "targets" && (
{filteredItems.map((item) => { + const targetItem = item as TargetItem; const isSelected = selectedIds.includes(item.id); return ( toggleSelection(item.id)} > - {item.name} + + {item.name} + {targetItem.isCrownJewel && ( + CJ + )} + ); })} diff --git a/src/features/operations/hooks/useTechniqueEditorForm.ts b/src/features/operations/hooks/useTechniqueEditorForm.ts index 0a77784..846194d 100644 --- a/src/features/operations/hooks/useTechniqueEditorForm.ts +++ b/src/features/operations/hooks/useTechniqueEditorForm.ts @@ -23,7 +23,7 @@ export const TechniqueEditorFormSchema = z.object({ sourceIp: z.string().optional(), targetSystems: z.string().optional(), offensiveToolIds: z.array(z.string()).default([]), - selectedCrownJewelIds: z.array(z.string()).default([]), + selectedTargetIds: z.array(z.string()).default([]), crownJewelAccess: z.union([z.literal("yes"), z.literal("no"), z.literal("")]).default(""), executionSuccess: z.union([z.literal("yes"), z.literal("no"), z.literal("")]).default(""), outcomes: z.object({ @@ -78,11 +78,11 @@ export type TechniqueEditorFormValues = z.infer void; onClose: () => void; }) { - const { operationId, existingTechnique, crownJewels, onSuccess, onClose } = params; + const { operationId, existingTechnique, targets, onSuccess, onClose } = params; const defaultValues = useMemo(() => { const fmt = (d?: Date | string | null) => { @@ -104,7 +104,7 @@ export function useTechniqueEditorForm(params: { sourceIp: "", targetSystems: "", offensiveToolIds: [], - selectedCrownJewelIds: [], + selectedTargetIds: [], crownJewelAccess: "", executionSuccess: "", outcomes: { @@ -119,7 +119,8 @@ export function useTechniqueEditorForm(params: { const prevention = existingTechnique.outcomes?.find((o) => o.type === "PREVENTION"); const attribution = existingTechnique.outcomes?.find((o) => o.type === "ATTRIBUTION"); - const selectedCJ = existingTechnique.crownJewelTargeted ? (crownJewels ?? []).map((cj) => cj.id) : []; + const crownJewelTargets = (targets ?? []).filter((target) => target.isCrownJewel).map((target) => target.id); + const selectedCJ = existingTechnique.crownJewelTargeted ? crownJewelTargets : []; return { description: existingTechnique.description ?? "", @@ -128,7 +129,7 @@ export function useTechniqueEditorForm(params: { sourceIp: existingTechnique.sourceIp ?? "", targetSystems: existingTechnique.targetSystem ?? "", offensiveToolIds: existingTechnique.tools?.map((t) => t.id) ?? [], - selectedCrownJewelIds: selectedCJ, + selectedTargetIds: selectedCJ, crownJewelAccess: existingTechnique.crownJewelCompromised ? "yes" : existingTechnique.crownJewelTargeted ? "no" : "", executionSuccess: existingTechnique.executedSuccessfully == null ? "" : existingTechnique.executedSuccessfully ? "yes" : "no", outcomes: { @@ -148,7 +149,7 @@ export function useTechniqueEditorForm(params: { }, }, }; - }, [existingTechnique, crownJewels]); + }, [existingTechnique, targets]); const form = useForm({ defaultValues, @@ -218,7 +219,9 @@ export function useTechniqueEditorForm(params: { description: values.description, sourceIp: values.sourceIp ?? undefined, targetSystem: values.targetSystems ?? undefined, - crownJewelTargeted: values.selectedCrownJewelIds.length > 0, + crownJewelTargeted: values.selectedTargetIds.some((id) => + (targets ?? []).some((target) => target.isCrownJewel && target.id === id), + ), crownJewelCompromised: values.crownJewelAccess === "yes", toolIds: values.offensiveToolIds, executedSuccessfully: values.executionSuccess === "" ? undefined : values.executionSuccess === "yes", diff --git a/src/features/settings/components/crown-jewels-tab.tsx b/src/features/settings/components/crown-jewels-tab.tsx deleted file mode 100644 index 8ab1167..0000000 --- a/src/features/settings/components/crown-jewels-tab.tsx +++ /dev/null @@ -1,189 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { api } from "@/trpc/react"; -import { Button, Input, Label } from "@components/ui"; -import SettingsHeader from "./settings-header"; -import InlineActions from "@components/ui/inline-actions"; -import { type CrownJewel } from "@prisma/client"; -import EntityListCard from "./entity-list-card"; -import EntityModal from "@components/ui/entity-modal"; -import ConfirmModal from "@components/ui/confirm-modal"; - -export default function CrownJewelsTab() { - const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); - const [editingJewel, setEditingJewel] = useState(null); - const [confirmDelete, setConfirmDelete] = useState(null); - - // Queries - const { data: crownJewels, isLoading } = api.taxonomy.crownJewels.list.useQuery(); - - // Mutations - const utils = api.useUtils(); - const createMutation = api.taxonomy.crownJewels.create.useMutation({ - onSuccess: () => { - void utils.taxonomy.crownJewels.invalidate(); - setIsCreateModalOpen(false); - }, - }); - - const updateMutation = api.taxonomy.crownJewels.update.useMutation({ - onSuccess: () => { - void utils.taxonomy.crownJewels.invalidate(); - setEditingJewel(null); - }, - }); - - const deleteMutation = api.taxonomy.crownJewels.delete.useMutation({ - onSuccess: () => { - void utils.taxonomy.crownJewels.invalidate(); - setConfirmDelete(null); - }, - }); - - const handleCreate = (data: { name: string; description: string }) => { - createMutation.mutate(data); - }; - - const handleUpdate = (id: string, data: { name?: string; description?: string }) => { - updateMutation.mutate({ id, ...data }); - }; - - const handleDelete = (id: string) => { - deleteMutation.mutate({ id }); - }; - - - if (isLoading) { - return ( -
-
Loading crown jewels...
-
- ); - } - - const jewels = (crownJewels ?? []) as Array; - - return ( -
- setIsCreateModalOpen(true)} /> - -
- {jewels.map((jewel) => ( - {jewel.name}} - description={jewel.description} - actions={ setEditingJewel(jewel)} onDelete={() => setConfirmDelete(jewel)} deleteDisabled={jewel.usageCount > 0} deleteDisabledReason={jewel.usageCount > 0 ? `In use by ${jewel.usageCount} operation(s)` : undefined} />} - /> - ))} - - {(!crownJewels || crownJewels.length === 0) && ( -
- No crown jewels configured. Use + New to create one. -
- )} -
- - {isCreateModalOpen && ( - setIsCreateModalOpen(false)} - isLoading={createMutation.isPending} - /> - )} - - {editingJewel && ( - handleUpdate(editingJewel.id, data)} - onClose={() => setEditingJewel(null)} - isLoading={updateMutation.isPending} - /> - )} - - {/* Delete confirmation */} - {confirmDelete && ( - handleDelete(confirmDelete.id)} - onCancel={() => setConfirmDelete(null)} - loading={deleteMutation.isPending} - /> - )} -
- ); -} - -interface CrownJewelEntityModalProps { - title: string; - initialData?: CrownJewel; - onSubmit: (data: { name: string; description: string }) => void; - onClose: () => void; - isLoading: boolean; -} - -function CrownJewelEntityModal({ title, initialData, onSubmit, onClose, isLoading }: CrownJewelEntityModalProps) { - const [name, setName] = useState(initialData?.name ?? ""); - const [description, setDescription] = useState(initialData?.description ?? ""); - - const handleSubmit = () => { - onSubmit({ name, description }); - }; - - return ( - - - - - )} - maxWidthClass="max-w-md" - > -
-
- - setName(e.target.value)} - placeholder="e.g., Customer Database, Payment System" - required - /> -
- -
- -