Claude 24f816b6a3
Consolidate 22 sibling repos into layered organism structure
Place useful parts of the surrounding repos into sica-fondt by layer, per the
body model (Ada = membrane; brain/endocrine/capabilities/knowledge non-Ada):

- brain/        LLM reasoning + providers (dapr, hermes, MoMoA)
- capabilities/ REPRAG sidecars: hermes tools/skills, dapr tools, parallel
                dispatch, A51 channels, and the OSINT cluster
- knowledge/    LORAG corpus: 754 cyber-skills, agency personas, secure-coding,
                MITRE ATT&CK data
- reference/    defensive threat-reference (C3, shhbruh doc) + AdaYaml parser

License handling: AGPL sources (worldosint, advanced_evolution, mercury,
Reticulum) and GPL DeTTECT are SPEC-only clean-room/port descriptions — no
copyleft code copied. MIT/Apache/data parts copied as working trees.

Safety: shhbruh escape/persistence material and C3 covert-C2 kept as reference
only, not wired into the running organism. See CONSOLIDATION.md.

https://claude.ai/code/session_01UehUqEXXJJCsHoA4voCU5c
2026-06-10 06:53:01 +00:00

237 lines
8.4 KiB
Python

#!/usr/bin/env python3
"""
CrowdStrike Falcon Deployment Verification Tool
Queries the CrowdStrike Falcon API to verify sensor deployment coverage,
identify unmanaged endpoints, and generate deployment status reports.
"""
import json
import sys
import os
import time
import csv
from datetime import datetime, timedelta
from urllib.request import Request, urlopen
from urllib.parse import urlencode
from urllib.error import HTTPError
FALCON_BASE_URL = os.environ.get("FALCON_BASE_URL", "https://api.crowdstrike.com")
FALCON_CLIENT_ID = os.environ.get("FALCON_CLIENT_ID", "")
FALCON_CLIENT_SECRET = os.environ.get("FALCON_CLIENT_SECRET", "")
def get_oauth_token() -> str:
"""Obtain OAuth2 bearer token from CrowdStrike API."""
url = f"{FALCON_BASE_URL}/oauth2/token"
data = urlencode({
"client_id": FALCON_CLIENT_ID,
"client_secret": FALCON_CLIENT_SECRET,
}).encode()
req = Request(url, data=data, method="POST")
req.add_header("Content-Type", "application/x-www-form-urlencoded")
with urlopen(req) as resp:
body = json.loads(resp.read())
return body["access_token"]
def api_get(token: str, endpoint: str, params: dict = None) -> dict:
"""Make authenticated GET request to Falcon API."""
url = f"{FALCON_BASE_URL}{endpoint}"
if params:
url += "?" + urlencode(params)
req = Request(url, method="GET")
req.add_header("Authorization", f"Bearer {token}")
req.add_header("Accept", "application/json")
with urlopen(req) as resp:
return json.loads(resp.read())
def get_all_host_ids(token: str) -> list:
"""Retrieve all host device IDs from Falcon."""
all_ids = []
offset = 0
limit = 5000
while True:
result = api_get(token, "/devices/queries/devices-scroll/v1", {
"limit": limit,
"offset": offset,
})
resources = result.get("resources", [])
if not resources:
break
all_ids.extend(resources)
offset += limit
if len(resources) < limit:
break
return all_ids
def get_host_details(token: str, host_ids: list) -> list:
"""Retrieve detailed host information for given IDs (batches of 100)."""
all_details = []
for i in range(0, len(host_ids), 100):
batch = host_ids[i:i + 100]
url = f"{FALCON_BASE_URL}/devices/entities/devices/v2"
data = json.dumps({"ids": batch}).encode()
req = Request(url, data=data, method="POST")
req.add_header("Authorization", f"Bearer {token}")
req.add_header("Content-Type", "application/json")
with urlopen(req) as resp:
body = json.loads(resp.read())
all_details.extend(body.get("resources", []))
return all_details
def analyze_deployment(hosts: list) -> dict:
"""Analyze deployment coverage and sensor health."""
now = datetime.utcnow()
stale_threshold = now - timedelta(days=7)
analysis = {
"total_hosts": len(hosts),
"os_breakdown": {},
"status_breakdown": {"online": 0, "offline": 0, "stale": 0},
"sensor_versions": {},
"rfm_hosts": [],
"stale_hosts": [],
"unprotected_hosts": [],
}
for host in hosts:
platform = host.get("platform_name", "Unknown")
analysis["os_breakdown"][platform] = analysis["os_breakdown"].get(platform, 0) + 1
version = host.get("agent_version", "Unknown")
analysis["sensor_versions"][version] = analysis["sensor_versions"].get(version, 0) + 1
status = host.get("status", "unknown")
last_seen = host.get("last_seen", "")
if last_seen:
try:
last_seen_dt = datetime.fromisoformat(last_seen.replace("Z", "+00:00")).replace(tzinfo=None)
if last_seen_dt < stale_threshold:
analysis["status_breakdown"]["stale"] += 1
analysis["stale_hosts"].append({
"hostname": host.get("hostname", ""),
"last_seen": last_seen,
"platform": platform,
})
elif status == "normal":
analysis["status_breakdown"]["online"] += 1
else:
analysis["status_breakdown"]["offline"] += 1
except (ValueError, TypeError):
analysis["status_breakdown"]["offline"] += 1
reduced_functionality = host.get("reduced_functionality_mode", "no")
if reduced_functionality == "yes":
analysis["rfm_hosts"].append({
"hostname": host.get("hostname", ""),
"reason": host.get("device_policies", {}).get("prevention", {}).get("policy_type", "unknown"),
})
prevention_policy = host.get("device_policies", {}).get("prevention", {})
if not prevention_policy.get("applied", False):
analysis["unprotected_hosts"].append({
"hostname": host.get("hostname", ""),
"platform": platform,
"reason": "Prevention policy not applied",
})
return analysis
def generate_deployment_report(analysis: dict, output_path: str) -> None:
"""Generate deployment status report."""
report = {
"report_generated": datetime.utcnow().isoformat() + "Z",
"deployment_summary": {
"total_managed_hosts": analysis["total_hosts"],
"online": analysis["status_breakdown"]["online"],
"offline": analysis["status_breakdown"]["offline"],
"stale_7_days": analysis["status_breakdown"]["stale"],
"in_rfm": len(analysis["rfm_hosts"]),
"unprotected": len(analysis["unprotected_hosts"]),
},
"os_distribution": analysis["os_breakdown"],
"sensor_version_distribution": analysis["sensor_versions"],
"hosts_requiring_attention": {
"stale_hosts": analysis["stale_hosts"][:50],
"rfm_hosts": analysis["rfm_hosts"][:50],
"unprotected_hosts": analysis["unprotected_hosts"][:50],
},
}
with open(output_path, "w", encoding="utf-8") as f:
json.dump(report, f, indent=2)
def export_stale_hosts_csv(stale_hosts: list, output_path: str) -> None:
"""Export stale hosts to CSV for remediation tracking."""
with open(output_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["Hostname", "Platform", "Last Seen", "Action Required"])
for host in stale_hosts:
writer.writerow([
host["hostname"],
host.get("platform", ""),
host["last_seen"],
"Investigate connectivity / reinstall sensor",
])
if __name__ == "__main__":
if not FALCON_CLIENT_ID or not FALCON_CLIENT_SECRET:
print("Error: Set FALCON_CLIENT_ID and FALCON_CLIENT_SECRET environment variables")
print()
print("Required environment variables:")
print(" FALCON_CLIENT_ID - API client ID from Falcon Console")
print(" FALCON_CLIENT_SECRET - API client secret")
print(" FALCON_BASE_URL - (Optional) API base URL (default: https://api.crowdstrike.com)")
sys.exit(1)
print("Authenticating with CrowdStrike Falcon API...")
token = get_oauth_token()
print("Retrieving host inventory...")
host_ids = get_all_host_ids(token)
print(f"Found {len(host_ids)} managed hosts")
print("Fetching host details...")
hosts = get_host_details(token, host_ids)
print("Analyzing deployment coverage...")
analysis = analyze_deployment(hosts)
report_path = "falcon_deployment_report.json"
generate_deployment_report(analysis, report_path)
print(f"\nDeployment report: {report_path}")
if analysis["stale_hosts"]:
csv_path = "falcon_stale_hosts.csv"
export_stale_hosts_csv(analysis["stale_hosts"], csv_path)
print(f"Stale hosts CSV: {csv_path}")
print(f"\n--- Deployment Summary ---")
print(f"Total hosts: {analysis['total_hosts']}")
print(f"Online: {analysis['status_breakdown']['online']}")
print(f"Offline: {analysis['status_breakdown']['offline']}")
print(f"Stale (>7 days): {analysis['status_breakdown']['stale']}")
print(f"In RFM: {len(analysis['rfm_hosts'])}")
print(f"Unprotected: {len(analysis['unprotected_hosts'])}")
print(f"\nOS Distribution: {json.dumps(analysis['os_breakdown'], indent=2)}")
print(f"Sensor Versions: {json.dumps(analysis['sensor_versions'], indent=2)}")