-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexporters.py
More file actions
105 lines (91 loc) · 3.54 KB
/
Copy pathexporters.py
File metadata and controls
105 lines (91 loc) · 3.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
from __future__ import annotations
import shutil
from datetime import date
from pathlib import Path
from typing import Any
import yaml
from .models import Finding, Inventory
OWASP_REFERENCES = {
"API1": "0xa1-broken-object-level-authorization",
"API2": "0xa2-broken-authentication",
"API3": "0xa3-broken-object-property-level-authorization",
"API4": "0xa4-unrestricted-resource-consumption",
"API5": "0xa5-broken-function-level-authorization",
"API6": "0xa6-unrestricted-access-to-sensitive-business-flows",
"API8": "0xa8-security-misconfiguration",
"API9": "0xa9-improper-inventory-management",
}
class ExportError(ValueError):
"""Raised when a report project cannot be exported."""
def export_redreport(
inventory: Inventory,
findings: list[Finding],
output: str | Path,
client: str,
) -> list[Path]:
root = Path(output)
if root.is_file() or (root.exists() and any(root.iterdir())):
raise ExportError(f"output already exists and is not empty: {root}")
findings_dir = root / "findings"
evidence_dir = root / "evidence"
findings_dir.mkdir(parents=True, exist_ok=True)
evidence_dir.mkdir(parents=True, exist_ok=True)
source = Path(inventory.source)
evidence = evidence_dir / f"openapi-source{source.suffix.lower() or '.yaml'}"
shutil.copy2(source, evidence)
report = {
"version": 1,
"engagement": {
"title": f"{inventory.title} — API Attack Surface Review",
"client": client,
"assessment_type": "OpenAPI Attack Surface Review",
"start_date": date.today(),
"end_date": date.today(),
"scope": inventory.servers,
"executive_summary": (
f"The OpenAPI review inventoried {len(inventory.endpoints)} operations and "
f"identified {len(findings)} deterministic security signal(s) requiring validation."
),
},
}
report_path = root / "report.yaml"
_write_yaml(report_path, report)
written = [report_path, evidence]
for index, finding in enumerate(findings, start=1):
finding_id = f"ASM-{index:03d}"
value: dict[str, Any] = {
"id": finding_id,
"title": finding.title,
"severity": finding.severity,
"status": "open",
"description": finding.description,
"impact": finding.impact,
"remediation": finding.remediation,
"affected_assets": [finding.asset],
"owasp": finding.owasp,
"evidence": [
{
"title": "OpenAPI contract evidence",
"path": f"evidence/{evidence.name}",
"description": (
f"Rule {finding.rule_id}; confidence {finding.confidence}; "
f"signal {finding.evidence}"
),
}
],
"references": [_owasp_reference(finding.owasp)],
}
if finding.cwe:
value["cwe"] = finding.cwe
output_path = findings_dir / f"{finding_id}.yaml"
_write_yaml(output_path, value)
written.append(output_path)
return written
def _owasp_reference(category: str) -> str:
key = category.split(":", 1)[0]
slug = OWASP_REFERENCES.get(key, "0x11-t10")
return f"https://owasp.org/API-Security/editions/2023/en/{slug}/"
def _write_yaml(path: Path, value: dict[str, Any]) -> None:
path.write_text(
yaml.safe_dump(value, sort_keys=False, allow_unicode=True), encoding="utf-8"
)