cloud security

Performing Cloud Asset Inventory with Cartography

Perform comprehensive cloud asset inventory and relationship mapping using Cartography to build a Neo4j security graph of infrastructure assets, IAM permissions, and attack paths across AWS, GCP, and Azure.

asset-inventoryattack-pathcartographycloud-securitycncfgraph-databaselyftneo4j
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

Cartography is a CNCF sandbox project (originally created at Lyft) that consolidates infrastructure assets and their relationships into a Neo4j graph database. It queries cloud APIs to discover resources, maps relationships between them, and enables security teams to identify attack paths, generate asset reports, and find areas for security improvement. The graph model reveals hidden connections such as IAM permission chains, network paths, and cross-account trust relationships.

When to Use

  • When conducting security assessments that involve performing cloud asset inventory with cartography
  • When following incident response procedures for related security events
  • When performing scheduled security testing or auditing activities
  • When validating security controls through hands-on testing

Prerequisites

  • Python 3.8+
  • Neo4j 4.x or 5.x database
  • Cloud provider credentials (AWS, GCP, Azure)
  • Docker (optional, for Neo4j deployment)
  • Minimum 4GB RAM for Neo4j, more for large environments

Installation

# Install Cartography
pip install cartography
 
# Verify installation
cartography --help

Deploy Neo4j with Docker

docker run -d \
  --name neo4j \
  -p 7474:7474 -p 7687:7687 \
  -e NEO4J_AUTH=neo4j/changethispassword \
  -e NEO4J_PLUGINS='["apoc"]' \
  -v neo4j_data:/data \
  neo4j:5-community

Running Cartography

Basic AWS Sync

# Sync AWS account data to Neo4j
cartography \
  --neo4j-uri bolt://localhost:7687 \
  --neo4j-user neo4j \
  --neo4j-password-env-var NEO4J_PASSWORD

Sync specific AWS modules

cartography \
  --neo4j-uri bolt://localhost:7687 \
  --neo4j-user neo4j \
  --neo4j-password-env-var NEO4J_PASSWORD \
  --aws-sync-all-profiles

GCP Sync

cartography \
  --neo4j-uri bolt://localhost:7687 \
  --neo4j-user neo4j \
  --neo4j-password-env-var NEO4J_PASSWORD \
  --gcp-requested-syncs compute iam storage

Security-Focused Cypher Queries

Find all S3 buckets with public access

MATCH (b:S3Bucket)
WHERE b.anonymous_access = true
   OR b.anonymous_actions IS NOT NULL
RETURN b.name, b.anonymous_actions, b.region, b.arn
ORDER BY b.name

Identify IAM users with admin policies

MATCH (user:AWSUser)-[:POLICY]->(policy:AWSPolicy)
WHERE policy.name = 'AdministratorAccess'
   OR policy.arn CONTAINS 'AdministratorAccess'
RETURN user.name, user.arn, policy.name, user.password_last_used

Find EC2 instances exposed to internet

MATCH (instance:EC2Instance)-[:MEMBER_OF_EC2_SECURITY_GROUP]->(sg:EC2SecurityGroup)
      -[:MEMBER_OF_EC2_SECURITY_GROUP_RULE]->(rule:IpRule)
WHERE rule.fromport <= 22 AND rule.toport >= 22
  AND rule.protocol IN ['tcp', '-1']
  AND '0.0.0.0/0' IN rule.ipranges
RETURN instance.instanceid, instance.publicipaddress, sg.groupid, sg.name

Discover cross-account trust relationships

MATCH (role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(principal:AWSPrincipal)
WHERE principal.arn CONTAINS ':root'
  AND NOT principal.arn CONTAINS role.accountid
RETURN role.arn, role.name, principal.arn AS trusted_account
ORDER BY role.name

Find attack path from public EC2 to sensitive S3

MATCH path = (instance:EC2Instance)-[:STS_ASSUME_ROLE_ALLOWS|MEMBER_OF_EC2_SECURITY_GROUP|
  POLICY|INSTANCE_PROFILE*1..5]->(bucket:S3Bucket)
WHERE instance.publicipaddress IS NOT NULL
  AND bucket.name CONTAINS 'sensitive'
RETURN path
LIMIT 25

Identify unused IAM roles

MATCH (role:AWSRole)
WHERE role.last_used IS NULL
   OR role.last_used < datetime().epochMillis - (90 * 24 * 60 * 60 * 1000)
RETURN role.name, role.arn, role.last_used
ORDER BY role.last_used

Find Lambda functions with overprivileged roles

MATCH (func:AWSLambda)-[:STS_ASSUME_ROLE_ALLOWS]->(role:AWSRole)-[:POLICY]->(policy:AWSPolicy)
WHERE policy.name = 'AdministratorAccess'
RETURN func.name, func.arn, role.name, policy.name

Network path analysis

MATCH (vpc:AWSVpc)-[:RESOURCE]->(subnet:EC2Subnet)-[:MEMBER_OF_SUBNET]->(instance:EC2Instance)
WHERE instance.publicipaddress IS NOT NULL
RETURN vpc.id, subnet.subnetid, subnet.cidr_block, instance.instanceid,
       instance.publicipaddress, instance.state

Scheduling Regular Syncs

Cron-based sync

# Add to crontab - sync every 6 hours
0 */6 * * * /usr/local/bin/cartography \
  --neo4j-uri bolt://localhost:7687 \
  --neo4j-user neo4j \
  --neo4j-password-env-var NEO4J_PASSWORD \
  >> /var/log/cartography/sync.log 2>&1

Docker Compose deployment

version: '3.8'
services:
  neo4j:
    image: neo4j:5-community
    ports:
      - "7474:7474"
      - "7687:7687"
    environment:
      NEO4J_AUTH: neo4j/securepwd123
      NEO4J_PLUGINS: '["apoc"]'
      NEO4J_dbms_memory_heap_max__size: 4G
    volumes:
      - neo4j_data:/data
 
  cartography:
    image: ghcr.io/cartography-cncf/cartography:latest
    depends_on:
      - neo4j
    environment:
      NEO4J_PASSWORD: securepwd123
      AWS_DEFAULT_REGION: us-east-1
    command: >
      --neo4j-uri bolt://neo4j:7687
      --neo4j-user neo4j
      --neo4j-password-env-var NEO4J_PASSWORD
 
volumes:
  neo4j_data:

Data Model Overview

Key Node Types

  • AWSAccount, GCPProject, AzureSubscription
  • EC2Instance, S3Bucket, RDSInstance, AWSLambda
  • AWSUser, AWSRole, AWSGroup, AWSPolicy
  • EC2SecurityGroup, EC2Subnet, AWSVpc
  • GCPInstance, GCSBucket, GCPRole

Key Relationship Types

  • RESOURCE: Account owns resource
  • POLICY: Principal has policy attached
  • STS_ASSUME_ROLE_ALLOWS: Principal can assume role
  • MEMBER_OF_EC2_SECURITY_GROUP: Instance belongs to SG
  • TRUSTS_AWS_PRINCIPAL: Cross-account trust

References

Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 3

api-reference.md4.9 KB

API Reference: Cartography Cloud Asset Inventory

Libraries Used

Library Purpose
neo4j Neo4j Python driver for graph database queries
subprocess Execute Cartography CLI sync commands
boto3 AWS SDK for supplementary asset lookups
json Parse Cartography output and Neo4j results

Installation

# Cartography
pip install cartography
 
# Neo4j Python driver
pip install neo4j
 
# Neo4j server (Docker)
docker run -d --name neo4j \
    -p 7474:7474 -p 7687:7687 \
    -e NEO4J_AUTH=neo4j/changeme \
    neo4j:5

Authentication

Neo4j Connection

from neo4j import GraphDatabase
import os
 
NEO4J_URI = os.environ.get("NEO4J_URI", "bolt://localhost:7687")
NEO4J_USER = os.environ.get("NEO4J_USER", "neo4j")
NEO4J_PASS = os.environ["NEO4J_PASS"]
 
driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASS))

AWS Credentials for Sync

# Cartography uses standard AWS credential chain
export AWS_PROFILE=security-audit
# Or: AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY

CLI Reference

Sync AWS Assets to Neo4j

cartography --neo4j-uri bolt://localhost:7687 \
    --neo4j-user neo4j \
    --neo4j-password-env-var NEO4J_PASS \
    --aws-sync-all-profiles

Sync Specific AWS Services

cartography --neo4j-uri bolt://localhost:7687 \
    --neo4j-user neo4j \
    --neo4j-password-env-var NEO4J_PASS \
    --aws-requested-syncs ec2:instances,iam:users,s3

Key CLI Flags

Flag Description
--neo4j-uri Neo4j Bolt URI
--neo4j-user Neo4j username
--neo4j-password-env-var Env var containing Neo4j password
--aws-sync-all-profiles Sync all configured AWS profiles
--aws-requested-syncs Sync specific AWS services
--gcp-requested-syncs Sync specific GCP services
--azure-sync-all-subscriptions Sync all Azure subscriptions
--statsd-enabled Enable StatsD metrics
--update-tag Custom tag for this sync run

Cypher Queries for Security Analysis

Find Public S3 Buckets

def find_public_s3_buckets(driver):
    query = """
    MATCH (s:S3Bucket)
    WHERE s.anonymous_access = true
    RETURN s.name AS bucket, s.region AS region, s.arn AS arn
    """
    with driver.session() as session:
        return [dict(r) for r in session.run(query)]

Find IAM Users Without MFA

def find_users_without_mfa(driver):
    query = """
    MATCH (u:AWSUser)
    WHERE NOT (u)-[:HAS_MFA_DEVICE]->(:MFADevice)
      AND u.password_enabled = true
    RETURN u.name AS username, u.arn AS arn,
           u.password_last_used AS last_login
    """
    with driver.session() as session:
        return [dict(r) for r in session.run(query)]

Find EC2 Instances with Public IPs and Permissive Security Groups

def find_exposed_instances(driver):
    query = """
    MATCH (i:EC2Instance)-[:MEMBER_OF_EC2_SECURITY_GROUP]->(sg:EC2SecurityGroup)
          -[:MEMBER_OF_IP_RULE]->(rule:IpRule)
    WHERE i.publicipaddress IS NOT NULL
      AND rule.cidr_ip = '0.0.0.0/0'
      AND rule.fromport <= 22
      AND rule.toport >= 22
    RETURN DISTINCT i.instanceid AS instance_id,
           i.publicipaddress AS public_ip,
           sg.name AS security_group
    """
    with driver.session() as session:
        return [dict(r) for r in session.run(query)]

Map Cross-Account IAM Trust Relationships

def find_cross_account_roles(driver):
    query = """
    MATCH (role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(principal:AWSPrincipal)
    WHERE principal.arn CONTAINS ':root'
      AND NOT principal.arn CONTAINS role.accountid
    RETURN role.arn AS role_arn,
           principal.arn AS trusted_principal,
           role.accountid AS role_account
    """
    with driver.session() as session:
        return [dict(r) for r in session.run(query)]

Find Unencrypted RDS Instances

def find_unencrypted_rds(driver):
    query = """
    MATCH (rds:RDSInstance)
    WHERE rds.storage_encrypted = false
    RETURN rds.db_instance_identifier AS db_name,
           rds.engine AS engine,
           rds.publicly_accessible AS public
    """
    with driver.session() as session:
        return [dict(r) for r in session.run(query)]

Cartography Node Types

Node Label Represents
AWSAccount AWS account
AWSUser IAM user
AWSRole IAM role
AWSPolicy IAM policy
EC2Instance EC2 instance
EC2SecurityGroup Security group
S3Bucket S3 bucket
RDSInstance RDS database instance
LambdaFunction Lambda function
EKSCluster EKS Kubernetes cluster

Output Format

{
  "sync_time": "2025-01-15T10:30:00Z",
  "accounts_synced": 3,
  "nodes_created": 12450,
  "relationships_created": 34200,
  "findings": {
    "public_s3_buckets": 2,
    "users_without_mfa": 5,
    "exposed_instances": 3,
    "cross_account_trusts": 8,
    "unencrypted_databases": 4
  }
}
standards.md0.5 KB

Standards - Cloud Asset Inventory with Cartography

CIS Controls v8

  • Control 1: Inventory and Control of Enterprise Assets
  • Control 2: Inventory and Control of Software Assets
  • Control 3: Data Protection
  • Control 6: Access Control Management

NIST 800-53

  • CM-8: System Component Inventory
  • PM-5: System Inventory
  • RA-2: Security Categorization

Cloud Security Alliance (CSA)

  • CCM AIS-01: Application and Interface Security
  • CCM DSP-01: Data Security and Privacy
  • CCM IAM-01: Identity and Access Management
workflows.md0.8 KB

Workflows - Cloud Asset Inventory with Cartography

Asset Discovery Workflow

1. Deploy Neo4j → Setup graph database
2. Configure Credentials → AWS/GCP/Azure access
3. Initial Sync → Run Cartography to populate graph
4. Query Analysis → Execute security-focused Cypher queries
5. Attack Path Review → Identify and document attack paths
6. Schedule Syncs → Automate regular inventory updates
7. Alert Integration → Notify on new risky relationships

Security Assessment Workflow

1. Populate Graph → Run full Cartography sync
2. Public Exposure → Query for internet-facing resources
3. IAM Analysis → Identify overprivileged identities
4. Cross-Account → Map trust relationships
5. Network Paths → Analyze network reachability
6. Report → Generate findings for remediation

Scripts 2

agent.py8.3 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Cartography cloud asset inventory agent.

Wraps the Cartography tool to enumerate and inventory cloud assets
across AWS accounts, then queries the resulting Neo4j graph database
to identify security-relevant relationships, exposed resources, and
misconfigured assets.
"""
import argparse
import json
import os
import subprocess
import sys
from datetime import datetime, timezone

try:
    from neo4j import GraphDatabase
    HAS_NEO4J = True
except ImportError:
    HAS_NEO4J = False


def run_cartography(profile=None, neo4j_uri="bolt://localhost:7687",
                    neo4j_user="neo4j", neo4j_password="neo4j"):
    """Execute Cartography to populate the Neo4j graph."""
    cmd = [sys.executable, "-m", "cartography",
           "--neo4j-uri", neo4j_uri,
           "--neo4j-user", neo4j_user,
           "--neo4j-password-env-var", "NEO4J_PASSWORD"]
    if profile:
        cmd.extend(["--aws-requested-syncs", "ec2,iam,s3,rds,lambda,ecs"])
    env = dict(os.environ)
    env["NEO4J_PASSWORD"] = neo4j_password
    print(f"[*] Running Cartography sync...")
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=900, env=env)
    if result.returncode != 0:
        print(f"[!] Cartography error: {result.stderr[:300]}", file=sys.stderr)
    return result.returncode


def query_neo4j(driver, query, params=None):
    """Execute a Cypher query against the Neo4j graph."""
    with driver.session() as session:
        result = session.run(query, params or {})
        return [record.data() for record in result]


def inventory_ec2_instances(driver):
    """Query EC2 instances from the graph."""
    query = """
    MATCH (i:EC2Instance)
    OPTIONAL MATCH (i)-[:MEMBER_OF_EC2_SECURITY_GROUP]->(sg:EC2SecurityGroup)
    RETURN i.id AS instance_id, i.instancetype AS type,
           i.state AS state, i.publicipaddress AS public_ip,
           i.privateipaddress AS private_ip,
           i.launchtime AS launch_time,
           collect(DISTINCT sg.groupid) AS security_groups
    ORDER BY i.launchtime DESC
    """
    return query_neo4j(driver, query)


def inventory_s3_buckets(driver):
    """Query S3 buckets from the graph."""
    query = """
    MATCH (b:S3Bucket)
    OPTIONAL MATCH (b)-[:RESOURCE]->(acl:S3Acl)
    RETURN b.name AS bucket_name, b.region AS region,
           b.anonymous_access AS anonymous_access,
           b.default_encryption AS encryption,
           b.versioning_status AS versioning
    ORDER BY b.name
    """
    return query_neo4j(driver, query)


def find_public_resources(driver):
    """Find publicly accessible resources."""
    findings = []
    # Public EC2 instances
    query = """
    MATCH (i:EC2Instance)
    WHERE i.publicipaddress IS NOT NULL AND i.state = 'running'
    MATCH (i)-[:MEMBER_OF_EC2_SECURITY_GROUP]->(sg:EC2SecurityGroup)
           -[:MEMBER_OF_EC2_SECURITY_GROUP]->(rule:IpRule)
    WHERE rule.cidr_ip = '0.0.0.0/0'
    RETURN DISTINCT i.id AS resource_id, 'EC2Instance' AS type,
           i.publicipaddress AS public_ip, rule.fromport AS port
    """
    for r in query_neo4j(driver, query):
        findings.append({
            "type": "public_ec2",
            "resource": r["resource_id"],
            "severity": "HIGH",
            "detail": f"Public IP {r.get('public_ip', 'N/A')} open on port {r.get('port', 'all')}",
        })

    # Public S3 buckets
    query = """
    MATCH (b:S3Bucket) WHERE b.anonymous_access = true
    RETURN b.name AS bucket_name
    """
    for r in query_neo4j(driver, query):
        findings.append({
            "type": "public_s3",
            "resource": r["bucket_name"],
            "severity": "CRITICAL",
            "detail": "S3 bucket allows anonymous access",
        })

    # IAM users without MFA
    query = """
    MATCH (u:AWSUser) WHERE u.mfa_active = false AND u.password_enabled = true
    RETURN u.name AS username, u.arn AS arn
    """
    for r in query_neo4j(driver, query):
        findings.append({
            "type": "iam_no_mfa",
            "resource": r["username"],
            "severity": "HIGH",
            "detail": f"IAM user {r['username']} has console access without MFA",
        })

    return findings


def find_unencrypted_resources(driver):
    """Find unencrypted storage resources."""
    findings = []
    query = """
    MATCH (v:EBSVolume) WHERE v.encrypted = false
    RETURN v.id AS volume_id, v.size AS size_gb, v.state AS state
    """
    for r in query_neo4j(driver, query):
        findings.append({
            "type": "unencrypted_ebs",
            "resource": r["volume_id"],
            "severity": "HIGH",
            "detail": f"Unencrypted EBS volume ({r.get('size_gb', '?')} GB)",
        })

    query = """
    MATCH (b:S3Bucket)
    WHERE b.default_encryption IS NULL OR b.default_encryption = false
    RETURN b.name AS bucket_name
    """
    for r in query_neo4j(driver, query):
        findings.append({
            "type": "unencrypted_s3",
            "resource": r["bucket_name"],
            "severity": "MEDIUM",
            "detail": "S3 bucket without default encryption",
        })

    return findings


def format_summary(ec2_instances, s3_buckets, security_findings):
    """Print inventory summary."""
    print(f"\n{'='*60}")
    print(f"  Cartography Cloud Asset Inventory")
    print(f"{'='*60}")
    print(f"  EC2 Instances    : {len(ec2_instances)}")
    print(f"  S3 Buckets       : {len(s3_buckets)}")
    print(f"  Security Findings: {len(security_findings)}")

    running = sum(1 for i in ec2_instances if i.get("state") == "running")
    public = sum(1 for i in ec2_instances if i.get("public_ip"))
    print(f"\n  EC2: {running} running, {public} with public IP")

    if security_findings:
        severity_counts = {}
        for f in security_findings:
            sev = f.get("severity", "INFO")
            severity_counts[sev] = severity_counts.get(sev, 0) + 1
        print(f"\n  Security Findings:")
        for sev in ["CRITICAL", "HIGH", "MEDIUM"]:
            count = severity_counts.get(sev, 0)
            if count:
                print(f"    {sev:10s}: {count}")
        for f in security_findings[:15]:
            print(f"    [{f['severity']:8s}] {f['type']:20s} | {f['resource']}: {f['detail'][:40]}")

    return {s: severity_counts.get(s, 0) for s in ["CRITICAL", "HIGH", "MEDIUM"]} if security_findings else {}


def main():
    parser = argparse.ArgumentParser(description="Cartography cloud asset inventory agent")
    parser.add_argument("--neo4j-uri", default="bolt://localhost:7687")
    parser.add_argument("--neo4j-user", default="neo4j")
    parser.add_argument("--neo4j-password", default=os.environ.get("NEO4J_PASSWORD", "neo4j"))
    parser.add_argument("--sync", action="store_true", help="Run Cartography sync before query")
    parser.add_argument("--profile", help="AWS CLI profile for sync")
    parser.add_argument("--output", "-o", help="Output JSON report")
    parser.add_argument("--verbose", "-v", action="store_true")
    args = parser.parse_args()

    if not HAS_NEO4J:
        print("[!] neo4j driver required: pip install neo4j", file=sys.stderr)
        sys.exit(1)

    if args.sync:
        run_cartography(args.profile, args.neo4j_uri, args.neo4j_user, args.neo4j_password)

    driver = GraphDatabase.driver(args.neo4j_uri, auth=(args.neo4j_user, args.neo4j_password))

    ec2_instances = inventory_ec2_instances(driver)
    s3_buckets = inventory_s3_buckets(driver)
    public_findings = find_public_resources(driver)
    encryption_findings = find_unencrypted_resources(driver)
    all_findings = public_findings + encryption_findings

    severity_counts = format_summary(ec2_instances, s3_buckets, all_findings)
    driver.close()

    report = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "tool": "Cartography",
        "ec2_instances": ec2_instances,
        "s3_buckets": s3_buckets,
        "security_findings": all_findings,
        "severity_counts": severity_counts,
        "risk_level": (
            "CRITICAL" if severity_counts.get("CRITICAL", 0) > 0
            else "HIGH" if severity_counts.get("HIGH", 0) > 0
            else "MEDIUM" if all_findings else "LOW"
        ),
    }

    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2, default=str)
        print(f"\n[+] Report saved to {args.output}")
    elif args.verbose:
        print(json.dumps(report, indent=2, default=str))


if __name__ == "__main__":
    main()
process.py6.2 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""
Cartography Cloud Asset Inventory Security Analysis Script

Connects to Neo4j after Cartography sync and runs security-focused
queries to identify misconfigurations, attack paths, and overprivileged access.
"""

import json
import sys
from datetime import datetime

try:
    from neo4j import GraphDatabase
except ImportError:
    print("Install neo4j driver: pip install neo4j")
    sys.exit(1)


SECURITY_QUERIES = {
    "public_s3_buckets": {
        "description": "S3 buckets with public access",
        "severity": "HIGH",
        "query": """
            MATCH (b:S3Bucket)
            WHERE b.anonymous_access = true
            RETURN b.name AS bucket, b.region AS region, b.arn AS arn
            ORDER BY b.name
        """
    },
    "admin_iam_users": {
        "description": "IAM users with administrator access",
        "severity": "HIGH",
        "query": """
            MATCH (user:AWSUser)-[:POLICY]->(policy:AWSPolicy)
            WHERE policy.name = 'AdministratorAccess'
            RETURN user.name AS username, user.arn AS arn, policy.name AS policy
        """
    },
    "public_ec2_ssh": {
        "description": "EC2 instances with SSH exposed to internet",
        "severity": "CRITICAL",
        "query": """
            MATCH (i:EC2Instance)-[:MEMBER_OF_EC2_SECURITY_GROUP]->(sg:EC2SecurityGroup)
                  -[:MEMBER_OF_EC2_SECURITY_GROUP_RULE]->(rule:IpRule)
            WHERE rule.fromport <= 22 AND rule.toport >= 22
              AND '0.0.0.0/0' IN rule.ipranges
            RETURN i.instanceid AS instance, i.publicipaddress AS public_ip,
                   sg.name AS security_group
        """
    },
    "cross_account_trusts": {
        "description": "Cross-account IAM trust relationships",
        "severity": "MEDIUM",
        "query": """
            MATCH (role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(p:AWSPrincipal)
            WHERE p.arn CONTAINS ':root'
              AND NOT p.arn CONTAINS role.accountid
            RETURN role.name AS role, role.arn AS role_arn,
                   p.arn AS trusted_principal
        """
    },
    "unencrypted_rds": {
        "description": "RDS instances without encryption",
        "severity": "HIGH",
        "query": """
            MATCH (rds:RDSInstance)
            WHERE rds.storage_encrypted = false
            RETURN rds.db_instance_identifier AS database,
                   rds.engine AS engine, rds.region AS region
        """
    },
    "unused_iam_roles": {
        "description": "IAM roles unused for 90+ days",
        "severity": "LOW",
        "query": """
            MATCH (role:AWSRole)
            WHERE role.last_used IS NULL
            RETURN role.name AS role, role.arn AS arn
            LIMIT 50
        """
    },
    "lambda_admin_roles": {
        "description": "Lambda functions with admin permissions",
        "severity": "HIGH",
        "query": """
            MATCH (f:AWSLambda)-[:STS_ASSUME_ROLE_ALLOWS]->(r:AWSRole)-[:POLICY]->(p:AWSPolicy)
            WHERE p.name = 'AdministratorAccess'
            RETURN f.name AS function_name, r.name AS role, p.name AS policy
        """
    }
}


def run_security_audit(uri, user, password):
    """Run all security queries against Neo4j."""
    driver = GraphDatabase.driver(uri, auth=(user, password))
    results = {}

    with driver.session() as session:
        for check_name, check_config in SECURITY_QUERIES.items():
            print(f"\n[*] Running: {check_config['description']} [{check_config['severity']}]")
            try:
                result = session.run(check_config["query"])
                records = [dict(record) for record in result]
                results[check_name] = {
                    "description": check_config["description"],
                    "severity": check_config["severity"],
                    "findings": records,
                    "count": len(records)
                }
                if records:
                    print(f"  [!] Found {len(records)} issues")
                    for r in records[:5]:
                        print(f"    - {json.dumps(r, default=str)}")
                    if len(records) > 5:
                        print(f"    ... and {len(records) - 5} more")
                else:
                    print(f"  [OK] No issues found")
            except Exception as e:
                print(f"  [ERROR] {e}")
                results[check_name] = {"error": str(e)}

    driver.close()
    return results


def generate_report(results, output_file=None):
    """Generate security audit report from query results."""
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    lines = [
        "=" * 60,
        "Cartography Cloud Asset Security Audit",
        f"Generated: {timestamp}",
        "=" * 60
    ]

    total_findings = sum(r.get("count", 0) for r in results.values() if "error" not in r)
    critical = sum(r.get("count", 0) for r in results.values() if r.get("severity") == "CRITICAL")
    high = sum(r.get("count", 0) for r in results.values() if r.get("severity") == "HIGH")

    lines.append(f"\nTotal Findings: {total_findings}")
    lines.append(f"Critical: {critical} | High: {high}")

    for name, data in results.items():
        if "error" in data:
            continue
        lines.append(f"\n## {data['description']} [{data['severity']}]")
        lines.append(f"Findings: {data['count']}")
        for f in data.get("findings", [])[:10]:
            lines.append(f"  - {json.dumps(f, default=str)}")

    report = "\n".join(lines)
    if output_file:
        with open(output_file, "w") as f:
            f.write(report)
        print(f"\n[+] Report saved to {output_file}")
    else:
        print(report)
    return report


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description="Cartography Security Audit")
    parser.add_argument("--neo4j-uri", default="bolt://localhost:7687")
    parser.add_argument("--neo4j-user", default="neo4j")
    parser.add_argument("--neo4j-password", required=True)
    parser.add_argument("--output", type=str, help="Output report file")
    args = parser.parse_args()

    results = run_security_audit(args.neo4j_uri, args.neo4j_user, args.neo4j_password)
    generate_report(results, args.output)

Assets 1

template.mdtext/markdown · 0.6 KB
Keep exploring