mirror of
https://github.com/BishopFox/cloudfox.git
synced 2026-11-09 01:29:32 -10:00
635 lines
19 KiB
Go
635 lines
19 KiB
Go
package aws
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/BishopFox/cloudfox/internal"
|
|
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/service/sts"
|
|
"github.com/dominikbraun/graph"
|
|
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type PmapperModule struct {
|
|
// General configuration data
|
|
Caller sts.GetCallerIdentityOutput
|
|
AWSRegions []string
|
|
AWSOutputType string
|
|
AWSTableCols string
|
|
|
|
Goroutines int
|
|
AWSProfile string
|
|
WrapTable bool
|
|
|
|
// Main module data
|
|
PmapperDataBasePath string
|
|
pmapperGraph graph.Graph[string, string]
|
|
Nodes []Node
|
|
Edges []Edge
|
|
CommandCounter internal.CommandCounter
|
|
// Used to store output data for pretty printing
|
|
output internal.OutputData2
|
|
modLog *logrus.Entry
|
|
}
|
|
|
|
type PmapperOutputRow struct {
|
|
Start string
|
|
End string
|
|
Paths []string
|
|
}
|
|
|
|
type Edge struct {
|
|
Source string `json:"source"`
|
|
Destination string `json:"destination"`
|
|
Reason string `json:"reason"`
|
|
ShortReason string `json:"short_reason"`
|
|
}
|
|
|
|
type Node struct {
|
|
Arn string `json:"arn"`
|
|
Type string
|
|
AccountID string
|
|
Name string
|
|
IDValue string `json:"id_value"`
|
|
AttachedPolicies []AttachedPolicies `json:"attached_policies"`
|
|
GroupMemberships []interface{} `json:"group_memberships"`
|
|
TrustPolicy interface{} `json:"trust_policy"`
|
|
TrustsDoc policy.TrustPolicyDocument
|
|
TrustedPrincipals []TrustedPrincipal
|
|
TrustedServices []TrustedService
|
|
TrustedFederatedProviders []TrustedFederatedProvider
|
|
InstanceProfile interface{} `json:"instance_profile"`
|
|
ActivePassword bool `json:"active_password"`
|
|
AccessKeys int `json:"access_keys"`
|
|
IsAdmin bool `json:"is_admin"`
|
|
PathToAdmin bool
|
|
PermissionsBoundary interface{} `json:"permissions_boundary"`
|
|
HasMfa bool `json:"has_mfa"`
|
|
Tags Tags `json:"tags"`
|
|
CanPrivEscToAdminString string
|
|
IsAdminString string
|
|
VendorName string
|
|
}
|
|
|
|
type TrustedPrincipal struct {
|
|
TrustedPrincipal string
|
|
ExternalID string
|
|
VendorName string
|
|
AccountIsInAnalyzedAccountList bool
|
|
//IsAdmin bool
|
|
//CanPrivEscToAdmin bool
|
|
}
|
|
|
|
type TrustedService struct {
|
|
TrustedService string
|
|
AccountID string
|
|
//IsAdmin bool
|
|
//CanPrivEscToAdmin bool
|
|
}
|
|
|
|
type TrustedFederatedProvider struct {
|
|
TrustedFederatedProvider string
|
|
ProviderAccountId string
|
|
ProviderShortName string
|
|
TrustedSubjects []string
|
|
//IsAdmin bool
|
|
//CanPrivEscToAdmin bool
|
|
}
|
|
|
|
type AttachedPolicies struct {
|
|
Arn string `json:"arn"`
|
|
Name string `json:"name"`
|
|
}
|
|
type Tags struct {
|
|
}
|
|
|
|
func (m *PmapperModule) initPmapperGraph() error {
|
|
// Parse mapper nodes and edges and populate the m.Nodes and m.Edges slices in the method struct
|
|
err := m.readPmapperData(m.Caller.Account)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
m.pmapperGraph = m.createAndPopulateGraph()
|
|
|
|
for i := range m.Nodes {
|
|
if m.doesNodeHavePathToAdmin(m.Nodes[i]) {
|
|
m.Nodes[i].PathToAdmin = true
|
|
m.Nodes[i].CanPrivEscToAdminString = "Yes"
|
|
//fmt.Println(m.Nodes[i].Arn, m.Nodes[i].IsAdmin, m.Nodes[i].PathToAdmin)
|
|
} else {
|
|
m.Nodes[i].PathToAdmin = false
|
|
m.Nodes[i].CanPrivEscToAdminString = "No"
|
|
}
|
|
if m.Nodes[i].IsAdmin {
|
|
m.Nodes[i].IsAdminString = "Yes"
|
|
} else {
|
|
m.Nodes[i].IsAdminString = "No"
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *PmapperModule) createAndPopulateGraph() graph.Graph[string, string] {
|
|
|
|
pmapperGraph := graph.New(graph.StringHash, graph.Directed())
|
|
|
|
// having issues with caching the graph. will have to swing back and try again later
|
|
|
|
// gob.Register(pmapperGraph)
|
|
// cacheKey := fmt.Sprintf("%s-pmapperGraph", aws.ToString(m.Caller.Account))
|
|
// cached, found := internal.Cache.Get(cacheKey)
|
|
// if found {
|
|
// return cached.(graph.Graph[string, string])
|
|
// }
|
|
|
|
for _, node := range m.Nodes {
|
|
_ = pmapperGraph.AddVertex(node.Arn)
|
|
|
|
}
|
|
|
|
for _, edge := range m.Edges {
|
|
err := pmapperGraph.AddEdge(
|
|
edge.Source,
|
|
edge.Destination,
|
|
graph.EdgeAttribute(edge.ShortReason, edge.Reason),
|
|
)
|
|
if err != nil {
|
|
if err == graph.ErrEdgeAlreadyExists {
|
|
// update the edge by copying the existing graph.Edge with attributes and add the new attributes
|
|
//fmt.Println("Edge already exists, but adding a new one!")
|
|
|
|
// get the existing edge
|
|
existingEdge, _ := pmapperGraph.Edge(edge.Source, edge.Destination)
|
|
// get the map of attributes
|
|
existingProperties := existingEdge.Properties
|
|
// add the new attributes to attributes map within the properties struct
|
|
// Check if the Attributes map is initialized, if not, initialize it
|
|
if existingProperties.Attributes == nil {
|
|
existingProperties.Attributes = make(map[string]string)
|
|
}
|
|
|
|
// Add or update the attribute
|
|
existingProperties.Attributes[edge.ShortReason] = edge.Reason
|
|
//Update the edge
|
|
pmapperGraph.UpdateEdge(
|
|
edge.Source,
|
|
edge.Destination,
|
|
graph.EdgeAttributes(existingProperties.Attributes),
|
|
)
|
|
|
|
}
|
|
//fmt.Println(edge.Reason)
|
|
}
|
|
|
|
}
|
|
|
|
//internal.Cache.Set(cacheKey, pmapperGraph, cache.DefaultExpiration)
|
|
return pmapperGraph
|
|
|
|
}
|
|
|
|
func (m *PmapperModule) DoesPrincipalHavePathToAdmin(principal string) bool {
|
|
for i := range m.Nodes {
|
|
if m.Nodes[i].Arn == principal {
|
|
if m.Nodes[i].PathToAdmin {
|
|
m.Nodes[i].CanPrivEscToAdminString = "Yes"
|
|
return true
|
|
} else {
|
|
m.Nodes[i].CanPrivEscToAdminString = "No"
|
|
return false
|
|
}
|
|
}
|
|
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m *PmapperModule) DoesPrincipalHaveAdmin(principal string) bool {
|
|
for i := range m.Nodes {
|
|
if m.Nodes[i].Arn == principal {
|
|
if m.Nodes[i].IsAdmin {
|
|
m.Nodes[i].IsAdminString = "Yes"
|
|
return true
|
|
} else {
|
|
m.Nodes[i].IsAdminString = "No"
|
|
return false
|
|
}
|
|
}
|
|
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m *PmapperModule) PrintPmapperData(outputDirectory string, verbosity int) {
|
|
// These struct values are used by the output module
|
|
m.output.Verbosity = verbosity
|
|
m.output.Directory = outputDirectory
|
|
m.output.CallingModule = "pmapper"
|
|
m.output.FullFilename = m.output.CallingModule
|
|
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
|
"module": m.output.CallingModule,
|
|
})
|
|
if m.AWSProfile == "" {
|
|
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
|
}
|
|
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
|
fmt.Printf("[%s][%s] Looking for pmapper data for this account and building a PrivEsc graph in golang if it exists.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
|
pmapperError := m.initPmapperGraph()
|
|
if pmapperError != nil {
|
|
fmt.Printf("[%s][%s] No pmapper data found for this account. \n\t\t\t1. Generate pmapper data by running `pmapper --profile %s graph create`\n\t\t\t2. After that completes, cloudfox will attempt to enrich this command and others with pmapper privesc data\n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.AWSProfile)
|
|
fmt.Printf("[%s][%s] For more info and troubleshooting steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
|
m.modLog.Error(pmapperError)
|
|
return
|
|
|
|
} else {
|
|
fmt.Printf("[%s][%s] Parsing pmapper data for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
|
|
|
}
|
|
|
|
m.output.Headers = []string{
|
|
"Account",
|
|
"Principal Arn",
|
|
"IsAdmin?",
|
|
"CanPrivEscToAdmin?",
|
|
}
|
|
|
|
// If the user specified table columns, use those.
|
|
// If the user specified -o wide, use the wide default cols for this module.
|
|
// Otherwise, use the hardcoded default cols for this module.
|
|
var tableCols []string
|
|
// If the user specified table columns, use those.
|
|
if m.AWSTableCols != "" {
|
|
// If the user specified wide as the output format, use these columns.
|
|
// remove any spaces between any commas and the first letter after the commas
|
|
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
|
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
|
tableCols = strings.Split(m.AWSTableCols, ",")
|
|
} else if m.AWSOutputType == "wide" {
|
|
tableCols = []string{
|
|
"Account",
|
|
"Principal Arn",
|
|
"IsAdmin?",
|
|
"CanPrivEscToAdmin?",
|
|
}
|
|
// Otherwise, use the default columns.
|
|
} else {
|
|
tableCols = []string{
|
|
"Principal Arn",
|
|
"IsAdmin?",
|
|
"CanPrivEscToAdmin?",
|
|
}
|
|
}
|
|
|
|
//Table rows
|
|
var isAdmin, pathToAdmin string
|
|
for i := range m.Nodes {
|
|
if m.Nodes[i].PathToAdmin || m.Nodes[i].IsAdmin {
|
|
if m.Nodes[i].IsAdmin {
|
|
isAdmin = "YES"
|
|
} else {
|
|
isAdmin = "No"
|
|
}
|
|
if m.Nodes[i].PathToAdmin {
|
|
pathToAdmin = "YES"
|
|
} else {
|
|
pathToAdmin = "No"
|
|
}
|
|
|
|
m.output.Body = append(
|
|
m.output.Body,
|
|
[]string{
|
|
aws.ToString(m.Caller.Account),
|
|
m.Nodes[i].Arn,
|
|
isAdmin,
|
|
pathToAdmin,
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
if len(m.output.Body) > 0 {
|
|
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
|
o := internal.OutputClient{
|
|
Verbosity: verbosity,
|
|
CallingModule: m.output.CallingModule,
|
|
Table: internal.TableClient{
|
|
Wrap: m.WrapTable,
|
|
},
|
|
Loot: internal.LootClient{
|
|
DirectoryName: m.output.FilePath,
|
|
},
|
|
}
|
|
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
|
Header: m.output.Headers,
|
|
Body: m.output.Body,
|
|
TableCols: tableCols,
|
|
Name: m.output.CallingModule,
|
|
})
|
|
o.PrefixIdentifier = m.AWSProfile
|
|
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
|
|
|
header, body := m.createPmapperTableData(outputDirectory)
|
|
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
|
Header: header,
|
|
Body: body,
|
|
Name: "pmapper-privesc-paths-enhanced",
|
|
SkipPrintToScreen: true,
|
|
})
|
|
|
|
loot := m.writeLoot(o.Table.DirectoryName, verbosity)
|
|
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{
|
|
Name: m.output.CallingModule,
|
|
Contents: loot,
|
|
})
|
|
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
|
|
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
|
|
|
fmt.Printf("[%s][%s] %s principals who are admin or have a path to admin identified.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
|
|
|
} else {
|
|
fmt.Printf("[%s][%s] No principals who are admin or have a path to admin identified. skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
|
}
|
|
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
|
}
|
|
|
|
func (m *PmapperModule) doesNodeHavePathToAdmin(startNode Node) bool {
|
|
if startNode.IsAdmin {
|
|
return true
|
|
} else {
|
|
for _, destNode := range m.Nodes {
|
|
if destNode.IsAdmin {
|
|
path, _ := graph.ShortestPath(m.pmapperGraph, startNode.Arn, destNode.Arn)
|
|
for _, p := range path {
|
|
if p != "" {
|
|
if startNode.Arn != destNode.Arn {
|
|
return true
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m *PmapperModule) createPmapperTableData(outputDirectory string) ([]string, [][]string) {
|
|
var header []string
|
|
var body [][]string
|
|
|
|
header = []string{
|
|
"Start",
|
|
"End",
|
|
"Path(s)",
|
|
}
|
|
|
|
var paths string
|
|
var admins, privescPathsBody [][]string
|
|
|
|
for _, startNode := range m.Nodes {
|
|
if startNode.IsAdmin {
|
|
admins = append(admins, []string{startNode.Arn, "", "ADMIN"})
|
|
|
|
} else {
|
|
for _, destNode := range m.Nodes {
|
|
if destNode.IsAdmin {
|
|
path, _ := graph.ShortestPath(m.pmapperGraph, startNode.Arn, destNode.Arn)
|
|
// if we have a path,
|
|
|
|
if len(path) > 0 {
|
|
if startNode.Arn != destNode.Arn {
|
|
paths = ""
|
|
// if we got herethere's a path. Lets print the reason and the short reason for each edge in the path to the screen
|
|
for i := 0; i < len(path)-1; i++ {
|
|
for _, edge := range m.Edges {
|
|
if edge.Source == path[i] && edge.Destination == path[i+1] {
|
|
|
|
//Some pmapper reasons have commas in them so lets get rid of them in the csvOutputdata
|
|
edge.Reason = strings.ReplaceAll(edge.Reason, ",", " and")
|
|
paths += fmt.Sprintf("%s %s %s\n", path[i], edge.Reason, path[i+1])
|
|
}
|
|
|
|
}
|
|
}
|
|
//trim the last newline from csvPaths
|
|
paths = strings.TrimSuffix(paths, "\n")
|
|
privescPathsBody = append(privescPathsBody, []string{startNode.Arn, destNode.Arn, paths})
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// create body by first adding the admins and then the privesc paths
|
|
body = append(body, admins...)
|
|
body = append(body, privescPathsBody...)
|
|
return header, body
|
|
|
|
}
|
|
|
|
func (m *PmapperModule) writeLoot(outputDirectory string, verbosity int) string {
|
|
path := filepath.Join(outputDirectory, "loot")
|
|
err := os.MkdirAll(path, os.ModePerm)
|
|
if err != nil {
|
|
m.modLog.Error(err.Error())
|
|
m.CommandCounter.Error++
|
|
panic(err.Error())
|
|
}
|
|
lootFilePath := filepath.Join(path, "pmapper.txt")
|
|
|
|
var admins, out string
|
|
|
|
for _, startNode := range m.Nodes {
|
|
if startNode.IsAdmin {
|
|
admins += fmt.Sprintf("ADMIN FOUND: %s\n", startNode.Arn)
|
|
} else {
|
|
for _, destNode := range m.Nodes {
|
|
if destNode.IsAdmin {
|
|
path, _ := graph.ShortestPath(m.pmapperGraph, startNode.Arn, destNode.Arn)
|
|
// if we have a path,
|
|
|
|
if len(path) > 0 {
|
|
if startNode.Arn != destNode.Arn {
|
|
// if we got here there is a path
|
|
out += fmt.Sprintf("PATH TO ADMIN FOUND\n Start: %s\n End: %s\n Path(s):\n", startNode.Arn, destNode.Arn)
|
|
//fmt.Println(path)
|
|
// if we got herethere's a path. Lets print the reason and the short reason for each edge in the path to the screen
|
|
for i := 0; i < len(path)-1; i++ {
|
|
for _, edge := range m.Edges {
|
|
if edge.Source == path[i] && edge.Destination == path[i+1] {
|
|
// print it like this: [start node] [reason] [end node]
|
|
out += fmt.Sprintf(" %s %s %s\n", path[i], edge.Reason, path[i+1])
|
|
}
|
|
// shortest path only finds the shortest path. We want to find all paths. So we need to find all paths that have the same start and end nodes from the path, but going back to the main edges slice
|
|
//for _, edge := range GlobalPmapperEdges {
|
|
// if edge.Source == path[i] && edge.Destination == path[i+1] {
|
|
// // print it like this: [start node] [reason] [end node]
|
|
// out += fmt.Sprintf(" %s %s %s\n", path[i], edge.Reason, path[i+1])
|
|
// }
|
|
// }
|
|
}
|
|
}
|
|
out += fmt.Sprintf("\n")
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
out = admins + "\n\n" + out
|
|
|
|
if verbosity > 2 {
|
|
fmt.Println()
|
|
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Beginning of loot file"))
|
|
fmt.Print(out)
|
|
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file"))
|
|
}
|
|
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), magenta(fmt.Sprintf("Loot file with ALL potential paths written to: [%s]", lootFilePath)))
|
|
return out
|
|
|
|
}
|
|
|
|
func (m *PmapperModule) readPmapperData(accountID *string) error {
|
|
|
|
var e, n string
|
|
if m.PmapperDataBasePath == "" {
|
|
e, n = generatePmapperDataBasePaths(accountID)
|
|
} else {
|
|
e = filepath.Join(m.PmapperDataBasePath, aws.ToString(accountID), "graph", "edges.json")
|
|
n = filepath.Join(m.PmapperDataBasePath, aws.ToString(accountID), "graph", "nodes.json")
|
|
}
|
|
|
|
nodesFile, err := os.Open(n)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer nodesFile.Close()
|
|
nodesByteValue, _ := ioutil.ReadAll(nodesFile)
|
|
json.Unmarshal([]byte(nodesByteValue), &m.Nodes)
|
|
|
|
edgesFile, err := os.Open(e)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer edgesFile.Close()
|
|
edgesByteValue, _ := ioutil.ReadAll(edgesFile)
|
|
json.Unmarshal([]byte(edgesByteValue), &m.Edges)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
func (m *PmapperModule) GenerateCypherStatements(goCtx context.Context, driver neo4j.DriverWithContext) error {
|
|
// Insert nodes
|
|
for i, node := range m.Nodes {
|
|
query, params := m.generateNodeCreateStatement(node, i)
|
|
if err := m.executeCypherQuery(goCtx, driver, query, params); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Insert edges
|
|
for i, edge := range m.Edges {
|
|
query, params := m.generateEdgeCreateStatement(edge, i)
|
|
if err := m.executeCypherQuery(goCtx, driver, query, params); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *PmapperModule) generateNodeCreateStatement(node Node, i int) (string, map[string]interface{}) {
|
|
var ptype, label, query string
|
|
var params map[string]any
|
|
|
|
if strings.Contains(node.Arn, "role") {
|
|
label = GetResourceNameFromArn(node.Arn)
|
|
ptype = "Role"
|
|
params = map[string]any{
|
|
"Id": node.Arn,
|
|
"ARN": node.Arn,
|
|
"Name": GetResourceNameFromArn(node.Arn),
|
|
"IdValue": node.IDValue,
|
|
"IsAdminP": node.IsAdmin,
|
|
"PathToAdmin": node.PathToAdmin,
|
|
}
|
|
|
|
} else if strings.Contains(node.Arn, "user") {
|
|
label = GetResourceNameFromArn(node.Arn)
|
|
ptype = "User"
|
|
//node.TrustPolicy = ""
|
|
params = map[string]any{
|
|
"Id": node.Arn,
|
|
"ARN": node.Arn,
|
|
"Name": GetResourceNameFromArn(node.Arn),
|
|
"IdValue": node.IDValue,
|
|
"IsAdminP": node.IsAdmin,
|
|
"PathToAdmin": node.PathToAdmin,
|
|
}
|
|
|
|
} else if strings.Contains(node.Arn, "group") {
|
|
label = GetResourceNameFromArn(node.Arn)
|
|
ptype = "Group"
|
|
}
|
|
label = strings.ReplaceAll(label, "-", "_")
|
|
label = strings.ReplaceAll(label, ".", "_")
|
|
|
|
query = `MERGE (%s:%s {Id: $Id, ARN: $ARN, Name: $Name, IdValue: $IdValue, IsAdminP: $IsAdminP, PathToAdmin: $PathToAdmin})`
|
|
|
|
//sanitizedArn := sanitizeArnForNeo4jLabel(node.Arn)
|
|
//id := fmt.Sprintf("%s_%s", sanitizedArn, ptype)
|
|
|
|
fmt.Println(fmt.Sprintf(query, label, ptype), params)
|
|
return fmt.Sprintf(query, label, ptype), params
|
|
}
|
|
|
|
func (m *PmapperModule) generateEdgeCreateStatement(edge Edge, i int) (string, map[string]interface{}) {
|
|
// Sanitize ARNs for matching nodes
|
|
//srcArnSanitized := sanitizeArnForNeo4jLabel(edge.Source)
|
|
//destArnSanitized := sanitizeArnForNeo4jLabel(edge.Destination)
|
|
|
|
query := `MATCH (a {ARN: $srcArn}), (b {ARN: $destArn}) CREATE (a)-[:CAN_ACCESS {reason: $reason, shortReason: $shortReason}]->(b)`
|
|
params := map[string]any{
|
|
"srcArn": edge.Source,
|
|
"destArn": edge.Destination,
|
|
"reason": edge.Reason,
|
|
"shortReason": edge.ShortReason,
|
|
}
|
|
fmt.Println(query, params)
|
|
return query, params
|
|
}
|
|
|
|
func (m *PmapperModule) executeCypherQuery(ctx context.Context, driver neo4j.DriverWithContext, query string, params map[string]interface{}) error {
|
|
_, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j"))
|
|
if err != nil {
|
|
sharedLogger.Errorf("Error executing query: %s -- %v", err, params)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sanitizeArnForNeo4jLabel(arn string) string {
|
|
// Replace non-allowed characters with underscores or other allowed characters
|
|
sanitized := strings.ReplaceAll(arn, ":", "_")
|
|
sanitized = strings.ReplaceAll(sanitized, "-", "_")
|
|
// Add more replacements if needed
|
|
return sanitized
|
|
}
|