mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-07-14 19:55:16 +03:00
240 lines
8.3 KiB
Python
240 lines
8.3 KiB
Python
#!/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()
|