From b681cf0c383234a8a5e6f72bb6fc9415e2020818 Mon Sep 17 00:00:00 2001 From: hotnops Date: Sun, 15 Sep 2024 21:22:47 -0700 Subject: [PATCH] Adding a basic panel to show which policies have a given action --- cmd/api/src/actions.go | 24 +++++++ cmd/api/src/queries/graph.go | 8 +++ cmd/api/src/server.go | 1 + .../src/components/ActionOverviewPanel.tsx | 47 +++++++++++- .../src/components/NodeOverviewPanel.tsx | 71 +++++++++++-------- ui/apeman-ui/src/services/actions.ts | 16 +++++ 6 files changed, 135 insertions(+), 32 deletions(-) create mode 100644 cmd/api/src/actions.go create mode 100644 ui/apeman-ui/src/services/actions.ts diff --git a/cmd/api/src/actions.go b/cmd/api/src/actions.go new file mode 100644 index 0000000..23220cd --- /dev/null +++ b/cmd/api/src/actions.go @@ -0,0 +1,24 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/hotnops/apeman/src/api/src/queries" +) + +func (s *Server) GetActionPolicies(c *gin.Context) { + actionName := "actionname" + action := c.Param(actionName) + + statements, err := queries.GetActionPolicies(s.ctx, s.db, action) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + } + + c.IndentedJSON(http.StatusOK, statements) +} + +func (s *Server) addActionsEndpoints(router *gin.RouterGroup) { + router.GET("policies", s.GetActionPolicies) +} diff --git a/cmd/api/src/queries/graph.go b/cmd/api/src/queries/graph.go index ba3b09e..a2feb0d 100644 --- a/cmd/api/src/queries/graph.go +++ b/cmd/api/src/queries/graph.go @@ -20,6 +20,14 @@ type PermissionMapping struct { Actions map[string][]graph.ID `json:"actions"` } +// Get all the policies attached to a particular action node +func GetActionPolicies(ctx context.Context, db graph.Database, action string) (graph.PathSet, error) { + query := "MATCH p=(a:AWSAction) <- [:ExpandsTo|Action*1..2] - (s:AWSStatement) - [:AttachedTo*2..3] - (pol:AWSManagedPolicy|AWSInlinePolicy) WHERE a.name = '%s' RETURN p" + query = fmt.Sprintf(query, action) + paths, err := CypherQueryPaths(ctx, db, query) + return paths, err +} + func GetInboundRolePaths(ctx context.Context, db graph.Database, roleId string) (graph.PathSet, error) { query := "MATCH p=(a:UniqueArn) - [:IdentityTransform* {name: 'sts:assumerole'}] -> (b:AWSRole) WHERE b.roleid = '%s' AND ALL(n IN nodes(p) WHERE SINGLE(x IN nodes(p) WHERE x = n)) RETURN p" query = fmt.Sprintf(query, roleId) diff --git a/cmd/api/src/server.go b/cmd/api/src/server.go index 7aaa1e2..5ac98f4 100644 --- a/cmd/api/src/server.go +++ b/cmd/api/src/server.go @@ -167,6 +167,7 @@ func (s *Server) handleRequests() { s.addNodeEndpoints(router.Group("/nodes/:nodeid")) s.addGroupsEndpoints(router.Group("/groups/:groupid")) s.addAccountsEndpoints(router.Group("/accounts/:accountid")) + s.addActionsEndpoints(router.Group("/actions/:actionname")) router.GET("/nodes", s.GetAWSNodes) router.GET("/accounts", s.GetAWSAccountIDs) diff --git a/ui/apeman-ui/src/components/ActionOverviewPanel.tsx b/ui/apeman-ui/src/components/ActionOverviewPanel.tsx index f672ec9..115519b 100644 --- a/ui/apeman-ui/src/components/ActionOverviewPanel.tsx +++ b/ui/apeman-ui/src/components/ActionOverviewPanel.tsx @@ -1,5 +1,48 @@ -const ActionOverviewPanel = () => { - return
ActionOverviewPanel
; +import { Accordion, Box } from "@chakra-ui/react"; +import PathAccordionList from "./PathAccordionList"; +import { addPathToGraph, Path } from "../services/pathService"; +import { useApemanGraph } from "../hooks/useApemanGraph"; +import { useEffect, useState } from "react"; +import { GetActionPolicies } from "../services/actions"; +import { getNodeLabel, Node } from "../services/nodeService"; + +interface Props { + node: Node; +} + +const ActionOverviewPanel = ({ node }: Props) => { + const [inboundPaths, setInboundPaths] = useState([]); + const { addNode, addEdge } = useApemanGraph(); + + useEffect(() => { + const { request, cancel } = GetActionPolicies(node.properties.map.name); + request + .then((res) => { + setInboundPaths(res.data.map((path) => path)); + }) + .catch((error) => { + if (error.code !== "ERR_CANCELED") { + console.error("Error fetching inbound roles:", error); + } + }); + + return cancel; + }, [node.properties.map.roleid]); + + return ( + + + { + addPathToGraph(n, addNode, addEdge); + }} + pathLabelFunction={(n) => getNodeLabel(n.Nodes[n.Nodes.length - 1])} + > + + + ); }; export default ActionOverviewPanel; diff --git a/ui/apeman-ui/src/components/NodeOverviewPanel.tsx b/ui/apeman-ui/src/components/NodeOverviewPanel.tsx index f94861f..6b9b203 100644 --- a/ui/apeman-ui/src/components/NodeOverviewPanel.tsx +++ b/ui/apeman-ui/src/components/NodeOverviewPanel.tsx @@ -10,6 +10,7 @@ import { kinds } from "../services/nodeService"; import PolicyOverview from "./PolicyOverview"; import UserOverviewPanel from "./UserOverviewPanel"; import GroupOverviewPanel from "./GroupOverviewPanel"; +import ActionOverviewPanel from "./ActionOverviewPanel"; interface Props { node: Node; @@ -27,6 +28,7 @@ const NodeOverviewPanel = ({ node }: Props) => { [kinds.AWSGroup, "Group Overview"], [kinds.UniqueArn, "Resource Overview"], [kinds.AWSStatement, "Statement Overview"], + [kinds.AWSAction, "Action Overview"], ]); return ( @@ -39,41 +41,50 @@ const NodeOverviewPanel = ({ node }: Props) => { size="sm" > - {nodeKinds.map((kind) => ( - - {tabTitleMap.get(kind)} - - ))} + {nodeKinds.map( + (kind) => + tabTitleMap.get(kind) && ( + + {tabTitleMap.get(kind)} + + ) + )} Node Overview - {nodeKinds.map((kind) => ( - - {kind === kinds.AWSAccount ? ( - - ) : null} - {kind === kinds.AWSRole ? ( - - ) : null} - {kind === kinds.AWSUser ? ( - - ) : null} - {kind === kinds.AWSGroup ? ( - - ) : null} - {kind === kinds.UniqueArn ? ( - - ) : null} - {kind === kinds.AWSStatement ? ( - - ) : null} - {kind === kinds.AWSManagedPolicy ? ( - - ) : null} - - ))} + {nodeKinds.map( + (kind) => + tabTitleMap.get(kind) && ( + + {kind === kinds.AWSAccount ? ( + + ) : null} + {kind === kinds.AWSRole ? ( + + ) : null} + {kind === kinds.AWSUser ? ( + + ) : null} + {kind === kinds.AWSGroup ? ( + + ) : null} + {kind === kinds.UniqueArn ? ( + + ) : null} + {kind === kinds.AWSStatement ? ( + + ) : null} + {kind === kinds.AWSManagedPolicy ? ( + + ) : null} + {kind === kinds.AWSAction ? ( + + ) : null} + + ) + )} diff --git a/ui/apeman-ui/src/services/actions.ts b/ui/apeman-ui/src/services/actions.ts new file mode 100644 index 0000000..4dfcb21 --- /dev/null +++ b/ui/apeman-ui/src/services/actions.ts @@ -0,0 +1,16 @@ +import apiClient from "./api-client"; +import { Path } from "./pathService"; + +export function GetActionPolicies(actionName: string) { + const controller = new AbortController(); + const request = apiClient.get(`/actions/${actionName}/policies`, { + signal: controller.signal, + }); + + return { + request, + cancel: () => { + controller.abort(); + }, + }; +} \ No newline at end of file