Documentation
¶
Overview ¶
Package graph implements an internal Asset Graph engine for DevOps-Proxy. It models infrastructure relationships across Kubernetes and AWS resources, providing a reusable foundation for attack path reasoning, AI explanations, drift detection, and future SaaS backend integration.
The graph is built from collected cluster inventory (KubernetesClusterData) and encodes real topological relationships — Service selectors, pod ownership, ServiceAccount bindings, and IRSA role associations — without consulting rule findings or heuristics.
Index ¶
- func EnrichWithAssumeRoleEdges(g *Graph, roleAssumptions map[string][]models.AssumableRole)
- func EnrichWithCloudAccess(g *Graph, roleAccess map[string][]models.RoleCloudAccess)
- func EnrichWithNodeRoles(g *Graph, nodeRoles map[string]string)
- func ResolveStartNode(input string) (nodeID string, ok bool)
- type BlastResult
- type Edge
- type EdgeType
- type Graph
- func (g *Graph) AddEdge(from, to string, edgeType EdgeType)
- func (g *Graph) AddNode(node *Node)
- func (g *Graph) EdgesFrom(id string) []*Edge
- func (g *Graph) EdgesTo(id string) []*Edge
- func (g *Graph) GetNode(id string) *Node
- func (g *Graph) HasEdge(from, to string) bool
- func (g *Graph) Neighbors(id string) []*Node
- type Node
- type NodeType
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func EnrichWithAssumeRoleEdges ¶ added in v0.16.0
func EnrichWithAssumeRoleEdges(g *Graph, roleAssumptions map[string][]models.AssumableRole)
EnrichWithAssumeRoleEdges extends an existing Graph by adding ASSUME_ROLE edges between IAMRole nodes based on sts:AssumeRole permissions discovered from the source role's policies.
roleAssumptions maps source IAM role ARNs to the list of target roles they can assume (as detected by iam.ResolveAssumableRoles). Only source roles that already have an IAMRole node in g have edges added — enrichment never creates dangling edges from unknown source nodes. Target roles not yet present are added as new IAMRole nodes.
Duplicate nodes and edges are deduplicated by the graph itself.
func EnrichWithCloudAccess ¶
func EnrichWithCloudAccess(g *Graph, roleAccess map[string][]models.RoleCloudAccess)
EnrichWithCloudAccess extends an existing Graph by adding cloud resource nodes (S3Bucket, SecretsManagerSecret, DynamoDBTable, KMSKey) and CAN_ACCESS edges from IAMRole nodes to those resources.
roleAccess maps IAM role ARNs (e.g. "arn:aws:iam::123:role/app-role") to the set of AWS resources that role can access, as resolved by internal/providers/aws/iam.ResolveRoleResourceAccess.
If the IAMRole node for a given ARN does not exist in g, that entry is silently skipped — enrichment never creates dangling edges. Duplicate cloud resource nodes and edges are deduplicated by the graph itself.
func EnrichWithNodeRoles ¶
EnrichWithNodeRoles extends an existing Graph by adding IAMRole nodes for AWS instance profile roles and ASSUMES_ROLE edges from the corresponding Node nodes to those IAMRole nodes.
nodeRoles maps Kubernetes node names (e.g. "ip-10-0-1-1.ec2.internal") to the IAM role ARN attached to the node's EC2 instance profile (e.g. "arn:aws:iam::123456789012:role/eks-node-role").
If the Node graph node for a given Kubernetes node name does not exist in g, that entry is silently skipped — enrichment never creates dangling edges. Duplicate IAMRole nodes and edges are deduplicated by the graph itself.
func ResolveStartNode ¶
ResolveStartNode converts a user-facing resource reference of the form "kind/name" into a graph node ID using the same sanitizeID conventions as BuildAssetGraph.
Supported kinds (case-insensitive):
deployment, statefulset, daemonset, job, cronjob, serviceaccount
Returns (nodeID, true) on success, or ("", false) when the kind prefix is not recognised or the name is empty.
Types ¶
type BlastResult ¶
type BlastResult struct {
// StartNodeID is the graph node ID used as the traversal origin.
StartNodeID string
// StartNode is the Node corresponding to StartNodeID; never nil when
// ComputeBlastRadius returns a non-error result.
StartNode *Node
// Identities contains all IAMRole nodes reachable from the start node.
// Sorted by Name ascending for deterministic output.
Identities []*Node
// Resources maps each cloud resource NodeType to the nodes of that type
// reachable from the start node. Only types with at least one reachable
// node are present as keys. Slices are sorted by Name ascending.
Resources map[NodeType][]*Node
}
BlastResult holds the outcome of a blast-radius computation: the set of IAM identity nodes and cloud resource nodes reachable from a starting workload or service account node via RUNS_AS → ASSUMES_ROLE → CAN_ACCESS traversal.
func ComputeBlastRadius ¶
func ComputeBlastRadius(g *Graph, startNodeID string) (*BlastResult, error)
ComputeBlastRadius performs a BFS from startNodeID over RUNS_AS, ASSUMES_ROLE, and CAN_ACCESS edges.
It collects:
- IAMRole nodes into BlastResult.Identities
- S3Bucket / SecretsManagerSecret / DynamoDBTable / KMSKey nodes into BlastResult.Resources, keyed by NodeType
Cloud resource nodes are treated as leaf nodes: once collected they are not enqueued for further traversal.
Returns an error when startNodeID does not exist in g. Returns a non-nil BlastResult (with empty Identities/Resources) when the start node exists but no relevant nodes are reachable.
type Edge ¶
type Edge struct {
// From is the source Node ID.
From string
// To is the destination Node ID.
To string
// Type describes the semantic relationship.
Type EdgeType
}
Edge is a directional relationship between two Nodes.
type EdgeType ¶
type EdgeType string
EdgeType describes the relationship direction between two Nodes.
const ( // EdgeTypeExposes: Internet → LoadBalancer — the service is publicly // reachable from outside the cluster. EdgeTypeExposes EdgeType = "EXPOSES" // EdgeTypeRoutesTo: LoadBalancer → Workload — the Service's selector // matches the workload's pod labels. EdgeTypeRoutesTo EdgeType = "ROUTES_TO" // EdgeTypeRunsAs: Workload → ServiceAccount — pods in this workload // are bound to the ServiceAccount. EdgeTypeRunsAs EdgeType = "RUNS_AS" // EdgeTypeAssumesRole: ServiceAccount → IAMRole — the ServiceAccount // carries an IRSA annotation granting it permission to assume the // named AWS IAM role. EdgeTypeAssumesRole EdgeType = "ASSUMES_ROLE" // EdgeTypeContains: Namespace → Workload or Namespace → ServiceAccount — // the namespace is the ownership boundary for the child resource. EdgeTypeContains EdgeType = "CONTAINS" // EdgeTypePartOf: Workload → Namespace — the workload belongs to the // namespace (inverse of CONTAINS; reserved for future use). EdgeTypePartOf EdgeType = "PART_OF" // EdgeTypeCanAccess: IAMRole → Cloud Resource — the IAM role's attached // policies grant access to the target AWS resource (S3, Secrets Manager, // DynamoDB, KMS). Added by graph.EnrichWithCloudAccess (Phase 12). EdgeTypeCanAccess EdgeType = "CAN_ACCESS" // EdgeTypeRunsOn: Workload → Node — pods in this workload are scheduled on // the Kubernetes worker node. Added in Phase 14 to support instance-profile // attack paths (Workload → Node → IAMRole → Cloud Resource). EdgeTypeRunsOn EdgeType = "RUNS_ON" // EdgeTypeAssumeRole: IAMRole_A → IAMRole_B — role A's policies grant // sts:AssumeRole on role B, enabling cross-role privilege escalation. // Added in Phase 16.1 to model multi-hop IAM escalation paths. EdgeTypeAssumeRole EdgeType = "ASSUME_ROLE" )
type Graph ¶
type Graph struct {
// Nodes maps node ID → *Node for O(1) lookup.
Nodes map[string]*Node
// Edges holds all directed relationships in insertion order.
Edges []*Edge
// contains filtered or unexported fields
}
Graph is the in-memory asset graph. Nodes are deduplicated by ID; Edges are deduplicated by (From, To, Type).
func BuildAssetGraph ¶
func BuildAssetGraph(cluster *models.KubernetesClusterData) (*Graph, error)
BuildAssetGraph converts collected Kubernetes cluster inventory into an Asset Graph that encodes real infrastructure relationships. It does not consult rule findings or heuristics — every edge reflects an actual API-level relationship in the cluster.
Node ID format (consistent with internal/render/graph.go):
- Internet: "Internet"
- LoadBalancer: sanitize("LoadBalancer_" + svc.Name)
- Workload: sanitize(pod.WorkloadKind + "_" + pod.WorkloadName)
- ServiceAccount: sanitize("ServiceAccount_" + sa.Name)
- IAMRole: sanitize("IAMRole_" + roleName)
- Namespace: sanitize("Namespace_" + ns.Name)
Edges built:
Internet → LoadBalancer (EXPOSES) — for every LB-type Service LoadBalancer → Workload (ROUTES_TO) — selector ∩ pod labels match Workload → ServiceAccount (RUNS_AS) — pod.ServiceAccountName ServiceAccount → IAMRole (ASSUMES_ROLE) — eks.amazonaws.com/role-arn Namespace → Workload (CONTAINS) Namespace → ServiceAccount (CONTAINS)
func (*Graph) AddEdge ¶
AddEdge inserts a directed edge (from → to) of the given type. If the exact same (from, to, type) triple already exists the call is a no-op. Silently ignores edges whose from or to node IDs are not present in the graph.
func (*Graph) AddNode ¶
AddNode inserts node into the graph. If a node with the same ID already exists the call is a no-op (first write wins).
type Node ¶
type Node struct {
// ID is the stable, sanitized identifier used as a graph key.
// Format: "{NodeType}_{sanitized_name}", e.g. "LoadBalancer_web_svc".
ID string
// Type classifies the entity (Internet, LoadBalancer, Workload, …).
Type NodeType
// Name is the human-readable resource name (e.g. "web-svc").
Name string
// Metadata carries optional key-value annotations such as "namespace",
// "kind" (for workloads), and "arn" (for IAM roles).
Metadata map[string]string
}
Node represents a security-relevant infrastructure entity in the asset graph.
type NodeType ¶
type NodeType string
NodeType identifies the kind of infrastructure entity a Node represents.
const ( // NodeTypeInternet is the conceptual external attacker entry point. NodeTypeInternet NodeType = "Internet" // NodeTypeLoadBalancer is a Kubernetes Service of type LoadBalancer. NodeTypeLoadBalancer NodeType = "LoadBalancer" // NodeTypeService is a Kubernetes Service (any type). NodeTypeService NodeType = "Service" // NodeTypeWorkload is a top-level workload controller (Deployment, // StatefulSet, DaemonSet, Job, CronJob, ReplicaSet, or Pod). NodeTypeWorkload NodeType = "Workload" // NodeTypeServiceAccount is a Kubernetes ServiceAccount. NodeTypeServiceAccount NodeType = "ServiceAccount" // NodeTypeIAMRole is an AWS IAM role reachable via IRSA. NodeTypeIAMRole NodeType = "IAMRole" // NodeTypeCluster represents the Kubernetes cluster itself (for // cluster-scoped control-plane resources such as EKS configuration). NodeTypeCluster NodeType = "Cluster" // NodeTypeNamespace is a Kubernetes namespace (containment boundary). NodeTypeNamespace NodeType = "Namespace" // NodeTypeS3Bucket is an Amazon S3 bucket reachable via an IAM role // (Phase 12 cloud reachability). NodeTypeS3Bucket NodeType = "S3Bucket" // NodeTypeSecretsManagerSecret is an AWS Secrets Manager secret reachable // via an IAM role (Phase 12 cloud reachability). NodeTypeSecretsManagerSecret NodeType = "SecretsManagerSecret" // NodeTypeDynamoDBTable is an Amazon DynamoDB table reachable via an IAM // role (Phase 12 cloud reachability). NodeTypeDynamoDBTable NodeType = "DynamoDBTable" // NodeTypeKMSKey is an AWS KMS key reachable via an IAM role // (Phase 12 cloud reachability). NodeTypeKMSKey NodeType = "KMSKey" // NodeTypeNode is a Kubernetes worker node (EC2 instance). Added in Phase 14 // to model instance-profile-based cloud access paths where pods reach AWS // through the node's IAM role rather than through IRSA. NodeTypeNode NodeType = "Node" // NodeTypeSSMParameter is an AWS Systems Manager Parameter Store entry // reachable via an IAM role (Phase 15 sensitivity classification). NodeTypeSSMParameter NodeType = "SSMParameter" )