npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
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 --helpDeploy 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-communityRunning Cartography
Basic AWS Sync
# Sync AWS account data to Neo4j
cartography \
--neo4j-uri bolt://localhost:7687 \
--neo4j-user neo4j \
--neo4j-password-env-var NEO4J_PASSWORDSync specific AWS modules
cartography \
--neo4j-uri bolt://localhost:7687 \
--neo4j-user neo4j \
--neo4j-password-env-var NEO4J_PASSWORD \
--aws-sync-all-profilesGCP Sync
cartography \
--neo4j-uri bolt://localhost:7687 \
--neo4j-user neo4j \
--neo4j-password-env-var NEO4J_PASSWORD \
--gcp-requested-syncs compute iam storageSecurity-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.nameIdentify 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_usedFind 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.nameDiscover 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.nameFind 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 25Identify 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_usedFind 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.nameNetwork 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.stateScheduling 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>&1Docker 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,AzureSubscriptionEC2Instance,S3Bucket,RDSInstance,AWSLambdaAWSUser,AWSRole,AWSGroup,AWSPolicyEC2SecurityGroup,EC2Subnet,AWSVpcGCPInstance,GCSBucket,GCPRole
Key Relationship Types
RESOURCE: Account owns resourcePOLICY: Principal has policy attachedSTS_ASSUME_ROLE_ALLOWS: Principal can assume roleMEMBER_OF_EC2_SECURITY_GROUP: Instance belongs to SGTRUSTS_AWS_PRINCIPAL: Cross-account trust
References
- Cartography GitHub: https://github.com/cartography-cncf/cartography
- Cartography Documentation: https://cartography.dev
- CNCF Sandbox Project
- Neo4j Cypher Query Language Reference
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:5Authentication
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_KEYCLI Reference
Sync AWS Assets to Neo4j
cartography --neo4j-uri bolt://localhost:7687 \
--neo4j-user neo4j \
--neo4j-password-env-var NEO4J_PASS \
--aws-sync-all-profilesSync 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,s3Key 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 relationshipsSecurity 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 remediationScripts 2
agent.py8.3 KB
#!/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
#!/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)