mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-09-03 06:50:51 +03:00
Expand 39 api-reference stubs with real tool docs, expand 15 agent.py boilerplate stubs
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user