#!/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()
