init commit
This commit is contained in:
6
.env.example
Normal file
6
.env.example
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Tanium API Gateway URL
|
||||||
|
# e.g. https://<customer>-api.cloud.tanium.com/plugin/products/gateway/graphql
|
||||||
|
TANIUM_API_URL=
|
||||||
|
|
||||||
|
# Tanium API Token
|
||||||
|
TANIUM_API_TOKEN=
|
||||||
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Output
|
||||||
|
output/
|
||||||
143
client.py
Normal file
143
client.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import time
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import config
|
||||||
|
from models import ComplianceFinding, CveFinding, EndpointCompliance
|
||||||
|
|
||||||
|
ENDPOINTS_QUERY = """
|
||||||
|
query getEndpoints($first: Int!, $after: String) {
|
||||||
|
endpoints(first: $first, after: $after) {
|
||||||
|
pageInfo {
|
||||||
|
endCursor
|
||||||
|
hasNextPage
|
||||||
|
}
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
computerID
|
||||||
|
ipAddress
|
||||||
|
os {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
compliance {
|
||||||
|
complianceFindings {
|
||||||
|
id
|
||||||
|
standard
|
||||||
|
standardVersion
|
||||||
|
state
|
||||||
|
ruleId
|
||||||
|
}
|
||||||
|
cveFindings {
|
||||||
|
cveId
|
||||||
|
cveYear
|
||||||
|
summary
|
||||||
|
severity
|
||||||
|
cvssScore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
MAX_RETRIES = 3
|
||||||
|
RETRY_BACKOFF = 2 # seconds, doubles each retry
|
||||||
|
|
||||||
|
|
||||||
|
def _make_request(query, variables):
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"session": config.TANIUM_API_TOKEN,
|
||||||
|
}
|
||||||
|
payload = {"query": query, "variables": variables}
|
||||||
|
|
||||||
|
for attempt in range(MAX_RETRIES):
|
||||||
|
resp = requests.post(config.TANIUM_API_URL, json=payload, headers=headers, timeout=60)
|
||||||
|
|
||||||
|
if resp.status_code == 429 or resp.status_code >= 500:
|
||||||
|
if attempt < MAX_RETRIES - 1:
|
||||||
|
wait = RETRY_BACKOFF * (2 ** attempt)
|
||||||
|
print(f" Retrying in {wait}s (HTTP {resp.status_code})...")
|
||||||
|
time.sleep(wait)
|
||||||
|
continue
|
||||||
|
resp.raise_for_status()
|
||||||
|
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
|
||||||
|
if "errors" in data:
|
||||||
|
error_messages = [e.get("message", str(e)) for e in data["errors"]]
|
||||||
|
raise RuntimeError(f"GraphQL errors: {'; '.join(error_messages)}")
|
||||||
|
|
||||||
|
return data["data"]
|
||||||
|
|
||||||
|
raise RuntimeError("Max retries exceeded")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_endpoint(node):
|
||||||
|
os_info = node.get("os") or {}
|
||||||
|
compliance = node.get("compliance") or {}
|
||||||
|
|
||||||
|
compliance_findings = [
|
||||||
|
ComplianceFinding(
|
||||||
|
id=f["id"],
|
||||||
|
standard=f["standard"],
|
||||||
|
standard_version=f["standardVersion"],
|
||||||
|
state=f["state"],
|
||||||
|
rule_id=f["ruleId"],
|
||||||
|
)
|
||||||
|
for f in (compliance.get("complianceFindings") or [])
|
||||||
|
]
|
||||||
|
|
||||||
|
cve_findings = [
|
||||||
|
CveFinding(
|
||||||
|
cve_id=f["cveId"],
|
||||||
|
cve_year=f["cveYear"],
|
||||||
|
summary=f["summary"],
|
||||||
|
severity=f["severity"],
|
||||||
|
cvss_score=f.get("cvssScore"),
|
||||||
|
)
|
||||||
|
for f in (compliance.get("cveFindings") or [])
|
||||||
|
]
|
||||||
|
|
||||||
|
return EndpointCompliance(
|
||||||
|
endpoint_id=node["id"],
|
||||||
|
name=node.get("name", ""),
|
||||||
|
computer_id=node.get("computerID", ""),
|
||||||
|
ip_address=node.get("ipAddress", ""),
|
||||||
|
os_name=os_info.get("name", ""),
|
||||||
|
compliance_findings=compliance_findings,
|
||||||
|
cve_findings=cve_findings,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_all_endpoints(page_size=500):
|
||||||
|
endpoints = []
|
||||||
|
cursor = None
|
||||||
|
page = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
page += 1
|
||||||
|
variables = {"first": page_size}
|
||||||
|
if cursor:
|
||||||
|
variables["after"] = cursor
|
||||||
|
|
||||||
|
print(f" Fetching page {page} (cursor: {cursor or 'start'})...")
|
||||||
|
data = _make_request(ENDPOINTS_QUERY, variables)
|
||||||
|
|
||||||
|
edges = data["endpoints"]["edges"]
|
||||||
|
page_info = data["endpoints"]["pageInfo"]
|
||||||
|
|
||||||
|
for edge in edges:
|
||||||
|
endpoints.append(_parse_endpoint(edge["node"]))
|
||||||
|
|
||||||
|
print(f" Got {len(edges)} endpoints (total: {len(endpoints)})")
|
||||||
|
|
||||||
|
if not page_info["hasNextPage"]:
|
||||||
|
break
|
||||||
|
|
||||||
|
cursor = page_info["endCursor"]
|
||||||
|
|
||||||
|
return endpoints
|
||||||
20
config.py
Normal file
20
config.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
TANIUM_API_URL = os.getenv("TANIUM_API_URL")
|
||||||
|
TANIUM_API_TOKEN = os.getenv("TANIUM_API_TOKEN")
|
||||||
|
|
||||||
|
|
||||||
|
def validate():
|
||||||
|
missing = []
|
||||||
|
if not TANIUM_API_URL:
|
||||||
|
missing.append("TANIUM_API_URL")
|
||||||
|
if not TANIUM_API_TOKEN:
|
||||||
|
missing.append("TANIUM_API_TOKEN")
|
||||||
|
if missing:
|
||||||
|
print(f"Error: Missing required environment variables: {', '.join(missing)}")
|
||||||
|
print("Copy .env.example to .env and fill in your values.")
|
||||||
|
sys.exit(1)
|
||||||
50
export.py
Normal file
50
export.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from dataclasses import asdict
|
||||||
|
|
||||||
|
|
||||||
|
def export_json(endpoints, filepath):
|
||||||
|
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||||
|
with open(filepath, "w") as f:
|
||||||
|
json.dump([asdict(ep) for ep in endpoints], f, indent=2)
|
||||||
|
print(f" JSON exported to {filepath}")
|
||||||
|
|
||||||
|
|
||||||
|
def export_csv(endpoints, output_dir):
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
compliance_path = os.path.join(output_dir, "compliance_findings.csv")
|
||||||
|
cve_path = os.path.join(output_dir, "cve_findings.csv")
|
||||||
|
|
||||||
|
# Compliance findings CSV
|
||||||
|
with open(compliance_path, "w", newline="") as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
writer.writerow([
|
||||||
|
"endpoint_id", "endpoint_name", "computer_id", "ip_address", "os_name",
|
||||||
|
"finding_id", "standard", "standard_version", "state", "rule_id",
|
||||||
|
])
|
||||||
|
for ep in endpoints:
|
||||||
|
for finding in ep.compliance_findings:
|
||||||
|
writer.writerow([
|
||||||
|
ep.endpoint_id, ep.name, ep.computer_id, ep.ip_address, ep.os_name,
|
||||||
|
finding.id, finding.standard, finding.standard_version,
|
||||||
|
finding.state, finding.rule_id,
|
||||||
|
])
|
||||||
|
print(f" Compliance findings CSV exported to {compliance_path}")
|
||||||
|
|
||||||
|
# CVE findings CSV
|
||||||
|
with open(cve_path, "w", newline="") as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
writer.writerow([
|
||||||
|
"endpoint_id", "endpoint_name", "computer_id", "ip_address", "os_name",
|
||||||
|
"cve_id", "cve_year", "summary", "severity", "cvss_score",
|
||||||
|
])
|
||||||
|
for ep in endpoints:
|
||||||
|
for finding in ep.cve_findings:
|
||||||
|
writer.writerow([
|
||||||
|
ep.endpoint_id, ep.name, ep.computer_id, ep.ip_address, ep.os_name,
|
||||||
|
finding.cve_id, finding.cve_year, finding.summary,
|
||||||
|
finding.severity, finding.cvss_score,
|
||||||
|
])
|
||||||
|
print(f" CVE findings CSV exported to {cve_path}")
|
||||||
66
main.py
Normal file
66
main.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
import config
|
||||||
|
from client import fetch_all_endpoints
|
||||||
|
from export import export_csv, export_json
|
||||||
|
|
||||||
|
|
||||||
|
def display_summary(endpoints):
|
||||||
|
total_compliance = sum(len(ep.compliance_findings) for ep in endpoints)
|
||||||
|
total_cve = sum(len(ep.cve_findings) for ep in endpoints)
|
||||||
|
|
||||||
|
print("\n=== Summary ===")
|
||||||
|
print(f" Endpoints: {len(endpoints)}")
|
||||||
|
print(f" Compliance findings: {total_compliance}")
|
||||||
|
print(f" CVE findings: {total_cve}")
|
||||||
|
|
||||||
|
if endpoints:
|
||||||
|
# Top 5 endpoints by CVE count
|
||||||
|
by_cve = sorted(endpoints, key=lambda ep: len(ep.cve_findings), reverse=True)
|
||||||
|
print("\n Top endpoints by CVE count:")
|
||||||
|
for ep in by_cve[:5]:
|
||||||
|
if ep.cve_findings:
|
||||||
|
print(f" {ep.name} ({ep.ip_address}): {len(ep.cve_findings)} CVEs")
|
||||||
|
|
||||||
|
# Severity breakdown
|
||||||
|
severity_counts = {}
|
||||||
|
for ep in endpoints:
|
||||||
|
for cve in ep.cve_findings:
|
||||||
|
severity_counts[cve.severity] = severity_counts.get(cve.severity, 0) + 1
|
||||||
|
if severity_counts:
|
||||||
|
print("\n CVE severity breakdown:")
|
||||||
|
for severity, count in sorted(severity_counts.items()):
|
||||||
|
print(f" {severity}: {count}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Fetch Tanium compliance and CVE findings")
|
||||||
|
parser.add_argument("--output-dir", default="./output", help="Directory for export files (default: ./output)")
|
||||||
|
parser.add_argument("--format", choices=["json", "csv", "both"], default="both", help="Export format (default: both)")
|
||||||
|
parser.add_argument("--page-size", type=int, default=500, help="Endpoints per page (default: 500)")
|
||||||
|
parser.add_argument("--display", action="store_true", help="Print summary table to console")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
config.validate()
|
||||||
|
|
||||||
|
print("Fetching endpoints with compliance data...")
|
||||||
|
endpoints = fetch_all_endpoints(page_size=args.page_size)
|
||||||
|
print(f"Fetched {len(endpoints)} endpoints.")
|
||||||
|
|
||||||
|
if args.display:
|
||||||
|
display_summary(endpoints)
|
||||||
|
|
||||||
|
if args.format in ("json", "both"):
|
||||||
|
print("\nExporting JSON...")
|
||||||
|
export_json(endpoints, os.path.join(args.output_dir, "endpoints.json"))
|
||||||
|
|
||||||
|
if args.format in ("csv", "both"):
|
||||||
|
print("\nExporting CSV...")
|
||||||
|
export_csv(endpoints, args.output_dir)
|
||||||
|
|
||||||
|
print("\nDone.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
31
models.py
Normal file
31
models.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ComplianceFinding:
|
||||||
|
id: str
|
||||||
|
standard: str
|
||||||
|
standard_version: str
|
||||||
|
state: str
|
||||||
|
rule_id: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CveFinding:
|
||||||
|
cve_id: str
|
||||||
|
cve_year: int
|
||||||
|
summary: str
|
||||||
|
severity: str
|
||||||
|
cvss_score: Optional[float]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EndpointCompliance:
|
||||||
|
endpoint_id: str
|
||||||
|
name: str
|
||||||
|
computer_id: str
|
||||||
|
ip_address: str
|
||||||
|
os_name: str
|
||||||
|
compliance_findings: list[ComplianceFinding] = field(default_factory=list)
|
||||||
|
cve_findings: list[CveFinding] = field(default_factory=list)
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
requests
|
||||||
|
python-dotenv
|
||||||
Reference in New Issue
Block a user