Conversation
…show it in the UI along
ttp_rbac_satisfied and ttp_exists_satisfied inspect campaign state and belong in the campaign crate, not the api crate. Moved both functions and their tests to a new campaign::ttp_applicability module; the api handler now imports them from there. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the nested 'system' object from the frontend payload by adding #[serde(flatten)] to the system field in Pod and K8sNode. SystemInfo fields (binaries, env_vars, files, ips, missing_files, mounts, processes, access_level) are now serialized directly into the entity map. Update state_conversions.rs accordingly: add_ui_system_field_aliases now reads env_vars and access_level from the top-level map, and prune_entity_payload_for_ui prunes access_level directly. Remove the now-unused prune_null_entries_json_map helper.
- output_parsers: add sys.has-binary(inner) pure parser
- single-arg literal path: derives binary name from path basename
- two-arg form: explicit-name, path (quoted names supported)
- ${OUTPUT} sentinel: extracts absolute paths from stdout
- routes through existing orchestrator merge path (no campaign mutation)
- external_parser: add binaries field to SystemFieldUpdates + merge
- execution: infer tool binary presence after successful execution
(mirrors Go analyzeToolSuccessfullyUsedInTTP; only writes when Unknown)
- domain: add listeners field to C2Server for exists:Listener TTP checks
- api: ttp_exists_satisfied checks C2Server.listeners; unknown kinds fail safe
- api: ttp_rbac_satisfied checks any captured SA (no SA = fail, not pass)
- AGENTS.md: note examples/ is test/playground material only
- Parser (`rawServiceAccountToken` effect): decodes the JWT payload from stdout, supports both projected (kubernetes.io claim) and legacy flat claim formats, and produces Namespace, ServiceAccount (with token), Pod, and K8sNode entities plus Contains/Uses/RunsOn relations. Uses the new SuccessWithFacts ParserOutput variant so entity-producing parsers don't need a system target. - ServiceAccountTokenAnalyzer: for every new ServiceAccount whose token carries pod_name claims, ensures the Pod entity exists with the correct service_account_name set and wires the Uses relation — the token-driven counterpart of ServiceAccountAnalyzer. - Rename PodServiceAccountAnalyzer → ServiceAccountAnalyzer throughout. - Add #[derive(Debug)] to FactsUpdate (required by new ParserOutput variant). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a TTP targets a service account (ns/.../sa/...) rather than a pod, resolve_exec_channel now traverses the `uses` relation (pod→SA) to find a backing pod and returns it as exec_target_id on ExecChannel. prepare_action uses exec_target_id as the ExecTtp.target_id so the builtin C2 kubectl-execs into the pod, not the SA. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A K8sNode always belongs to the cluster it was discovered in. Added NodeClusterAnalyzer (mirrors NamespaceClusterAnalyzer) to emit a contains relation from the campaign cluster to every new K8sNode, regardless of how it was discovered (SA token, RBAC get nodes, etc.). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
K8sNodes are always part of the cluster they are discovered in. Added ManagesNode relation type (domain) and NodeClusterRule (campaign) so that every new K8sNode — regardless of discovery path (SA token, RBAC get nodes, kubelet mounts, etc.) — gets a manages-node edge from the campaign cluster. The graph renderer treats manages-node as a high-priority compound-node parent, placing nodes inside the cluster. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added is<T>() and downcast<T>() methods on dyn Relation (all Send+Sync variants) so callers can check relation types structurally rather than comparing relation_name() strings. Updated all relation type checks in analyzers, rules, and output_parsers tests to use r.is::<Contains>() / r.is::<RunsOn>() etc. String-based checks on RelationSummary (campaign.relations) are unchanged since type information is not preserved in summaries. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Parse SelfSubjectRulesReview output (JSON and kubectl tabular) into RbacPermission entries on the target ServiceAccount - Resolve SA via pod→SA indirection: exec target_id is the pod, not the SA; follow pod.service_account_name to find the correct SA - Convert entitlements to camelCase `can` field in API state conversions so the frontend EntitlementInfo component receives the expected shape - Remove stale entityInfo.svelte block using old Go-style verbs/resourceTypes - Add drop_ran-ws.yaml TTP and defaults for drop_fileless_binary params - Fix read_environment_variables procedure key; add vite HMR config for Rust proxy dev workflow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a TTP fails with exit code 127, record the procedure's binary as BinaryPresence::Absent on the exec system so the procedure selector can automatically fall back to an alternative tool next time. - Add "exit code 127" string pattern to CommandNotFoundFailureAnalyzer to catch cases where kube embeds the exit code in fail_reason without propagating it to event.exit_code - Add procedure_binary_name() helper: tries procedure.tool, then bare procedure.id, then first word of command - On CommandNotFound failure, write Absent via the existing apply_system_update path (empty-path convention, "only if Unknown" guard preserves confirmed Present entries) - Add tests covering Absent marking and Present-not-overwritten invariant Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parses reverse DNS lookup CSV output (ip,ptr format) into Pod entities. Mirrors the Go parseReverseDnsLookup + analyzeDnsEntries logic: only processes cluster.local entries, identifies pod IPs by matching the first DNS label to the IP in kebab-case, and derives pod name/namespace from the DNS label count (4/5/6/6+ label patterns). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Exposes Ran's knowledge graph, armory, and execution engine as 15 MCP tools so that LLM agents can drive adversary emulation campaigns. Transport: Streamable HTTP at /mcp (same port as the API/SSE server). Tools: Discovery : get_graph, get_entity, get_attack_surface, resolve_workload Campaign : get_campaign_state, get_attack_flow Armory : list_ttps, get_applicable_ttps, get_ttp_detail Execution : execute_action, wait_for_result Goal eval : check_rbac_goal, check_access_level Extension : list_parse_audits, add_parser Changes: - crates/api/Cargo.toml : add rmcp 1.x with streamable-http-server feature - crates/api/src/mcp.rs : RanMcpHandler<S: ApiService> + McpConfig + mcp_router() - crates/api/src/lib.rs : pub mod mcp, McpConfig re-export, router_with_sse_and_mcp() - crates/api/src/api_handlers.rs : widen ttp_is_applicable_for_target_kind to pub(crate) - crates/cli/src/main.rs : construct McpConfig and switch to router_with_sse_and_mcp
Introduces TimelineStore with a discriminated union type (TtpActionEntry | EntityEntry) supporting ttp-action, discovery, credential, and access-gained entries. Replaces the flat ActionLogStore with richer state: pendingCount, open toggle, deduplication by entityId, and resolveTtpAction. All 13 tests pass.
Replaces the single-kind ActionLogDrawer with a multi-kind OperationTimeline that handles ttp-action, credential, access-gained, and discovery entries.
… correlation Replace ActionLogDrawer/actionLog with OperationTimeline/timeline store. Make sendAction and onExecuteTTP async so cmd_id from ExecuteAction response is used for per-command correlation instead of TTP definition id. Add entity-discovered SSE handler to feed timeline discovery events.
…bel branch Read cmdId (camelCase) from ExecuteAction response to match the serde rename in ExecuteActionAck, so TTP timeline entries resolve from pending instead of always falling back to randomUUID. Also remove the redundant K8sCredential branch in entryLabel that returned the same string as the fallback.
…ument dedup design
Magier
added a commit
that referenced
this pull request
May 30, 2026
feat: Rust port (oxidation) — full campaign engine, planner, MCP, and timeline UI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
crates/workspace withapi,app,armory,c2,campaign,cli,domain,graph,k8s, andplannercrates, replacing the Go implementation with a full Rust campaign engine, entity store, inference engine, and knowledge graphPendingViewk8s_requestprocedure abstraction, CopyFail CVE-2026-31431 escape-to-host TTP, new Discovery/LateralMovement/Tools TTPs, tool YAML slottingrmcpexposed from the API crateTimelineStorewithActionGroupeffect grouping, single-passeffectCounts, SSEentity-discoveredevents, Svelte 5 reactivity fixesTest Plan
cargo test— all suites passvitest run— 22 frontend tests pass/api/plan/executeendpoint with a sample YAML plan