Expand 39 api-reference stubs with real tool docs, expand 15 agent.py boilerplate stubs

This commit is contained in:
mukul975
2026-03-19 13:29:50 +01:00
parent d63b578a2f
commit d005ae764b
191 changed files with 4300 additions and 587 deletions
@@ -1,60 +1,239 @@
#!/usr/bin/env python3
"""Cartography cloud asset inventory agent."""
import argparse, json
"""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:
import requests
from neo4j import GraphDatabase
HAS_NEO4J = True
except ImportError:
requests = None
HAS_NEO4J = False
def run_scan(target, token=None):
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 = []
if not requests: return [{"error": "requests required"}]
headers = {"Authorization": f"Bearer {token}"} if token else {}
try:
resp = requests.get(f"{target}", headers=headers, timeout=15)
if resp.status_code == 200:
findings.append({"check": "Target Accessible", "status": "OK", "severity": "INFO"})
else:
findings.append({"check": "Target Access", "status": f"HTTP {resp.status_code}", "severity": "MEDIUM"})
except requests.RequestException as e:
findings.append({"error": str(e)})
# 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 analyze_results(target, token=None):
def find_unencrypted_resources(driver):
"""Find unencrypted storage resources."""
findings = []
if not requests: return []
headers = {"Authorization": f"Bearer {token}"} if token else {}
try:
resp = requests.get(f"{target}/api/v1/results", headers=headers, timeout=15)
if resp.status_code == 200:
data = resp.json()
for item in data.get("findings", data.get("results", [])):
severity = item.get("severity", item.get("risk", "MEDIUM"))
findings.append({"check": item.get("name", item.get("title", "unknown")),
"severity": severity.upper() if isinstance(severity, str) else "MEDIUM"})
except requests.RequestException:
pass
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():
p = argparse.ArgumentParser(description="Cartography cloud asset inventory agent")
p.add_argument("--target", required=True, help="Target URL or IP")
p.add_argument("--token", help="API token")
p.add_argument("--output", "-o", help="Output JSON report")
p.add_argument("--verbose", "-v", action="store_true")
a = p.parse_args()
print("[*] Cartography cloud asset inventory agent")
report = {"timestamp": datetime.now(timezone.utc).isoformat(), "target": a.target, "findings": []}
report["findings"].extend(run_scan(a.target, a.token))
report["findings"].extend(analyze_results(a.target, a.token))
high = sum(1 for f in report["findings"] if f.get("severity") in ("HIGH", "CRITICAL"))
report["risk_level"] = "CRITICAL" if high > 2 else "HIGH" if high else "MEDIUM" if report["findings"] else "LOW"
print(f"[*] {len(report['findings'])} findings, risk: {report['risk_level']}")
if a.output:
with open(a.output, "w") as f: json.dump(report, f, indent=2)
else:
print(json.dumps(report, indent=2))
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()