mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-11 13:44:56 +03:00
237 lines
8.4 KiB
Python
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)}")
|