CYBER-0 initial concept ready

This commit is contained in:
Ole Valente
2026-09-22 00:00:45 +02:00
parent 29eecd4f70
commit edb6cde069
70 changed files with 1976 additions and 3140 deletions
+15
View File
@@ -0,0 +1,15 @@
# OWASP ZAP Methodology
ZAP performs web application tests from ephemeral GitLab Kubernetes-executor pods. Targets come from the `web_applications` group in root `assets.yml`; authentication values come from environment-scoped GitLab variables.
The OWASP ASVS source material used to develop the finding-to-control mapping is retained under `reference/OWASP_ASVS`.
`run.sh` executes ZAP baseline scanning and a focused TLS preflight, because ZAP
does not enumerate all protocol and cipher weaknesses. `scripts/normalize.py`
maps both evidence sources to OWASP ASVS 5 controls using `asvs-mapping.json`.
Run with:
```bash
./methodologies/zap/run.sh https://target.example
```
+10
View File
@@ -0,0 +1,10 @@
{
"10020": ["3.4.6"],
"10021": ["3.2.1"],
"10035": ["3.4.1"],
"10036": ["13.2.1"],
"10038": ["3.4.3"],
"TLS-OLD-PROTOCOL": ["12.1.1"],
"TLS-SELF-SIGNED": ["12.2.2"],
"TLS-WEAK-CIPHER": ["12.1.1"]
}
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
TARGET_URL="${1:?Usage: run.sh https://target}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPOSITORY_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
ARTIFACT_ROOT="${ARTIFACT_ROOT:-$REPOSITORY_DIR/artifacts}"
RAW_DIR="$ARTIFACT_ROOT/raw/zaproxy"
NORMALIZED_DIR="$ARTIFACT_ROOT/normalized"
mkdir -p "$RAW_DIR" "$NORMALIZED_DIR"
zap-baseline.py -t "$TARGET_URL" -J "$RAW_DIR/zap.json" -I
python3 "$SCRIPT_DIR/scripts/tls-probe.py" "$TARGET_URL" "$RAW_DIR/tls.json"
if [ "${NORMALIZE_ZAP:-true}" = "true" ]; then
python3 "$SCRIPT_DIR/scripts/normalize.py" \
"$RAW_DIR/zap.json" \
"$RAW_DIR/tls.json" \
"$NORMALIZED_DIR/zaproxy-demo-web.json" \
--target "$TARGET_URL" \
--mapping "$SCRIPT_DIR/asvs-mapping.json" \
--schema "$REPOSITORY_DIR/schemas/test-report.schema.json"
fi
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""Normalize ZAP baseline alerts and TLS preflight findings with ASVS mappings."""
import argparse
import json
import os
from datetime import datetime, timezone
from pathlib import Path
from jsonschema import Draft202012Validator, FormatChecker
RISK = {"0": "info", "1": "low", "2": "medium", "3": "high", "4": "critical"}
def standards(mapping: dict[str, list[str]], finding_id: str) -> list[dict[str, str]]:
return [
{"framework": "OWASP ASVS", "version": "5.0", "control": control}
for control in mapping.get(finding_id, [])
]
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("zap_json", type=Path)
parser.add_argument("tls_json", type=Path)
parser.add_argument("output", type=Path)
parser.add_argument("--target", required=True)
parser.add_argument("--mapping", type=Path, default=Path("methodologies/zap/asvs-mapping.json"))
parser.add_argument("--schema", type=Path, default=Path("schemas/test-report.schema.json"))
args = parser.parse_args()
zap = json.loads(args.zap_json.read_text(encoding="utf-8"))
tls = json.loads(args.tls_json.read_text(encoding="utf-8"))
mapping = json.loads(args.mapping.read_text(encoding="utf-8"))
results = []
for site in zap.get("site", []):
for alert in site.get("alerts", []):
finding_id = str(alert.get("pluginid", alert.get("alertRef", "ZAP-UNKNOWN")))
instances = alert.get("instances", [])
observed = "; ".join(str(item.get("uri", "")) for item in instances[:5])
results.append(
{
"id": f"ZAP-{finding_id}",
"title": str(alert.get("alert", "ZAP finding")),
"description": str(alert.get("desc", "")),
"status": "failed",
"severity": RISK.get(str(alert.get("riskcode", "0")), "info"),
"category": "Web application security",
"expected": "No ZAP alert",
"observed": observed or str(alert.get("evidence", "")),
"remediation": str(alert.get("solution", "Review and remediate the finding.")),
"standards": standards(mapping, finding_id),
"evidence": [{"type": "text", "name": "ZAP alert", "value": observed or str(alert)}],
}
)
for finding in tls.get("findings", []):
finding_id = str(finding["id"])
results.append(
{
"id": finding_id,
"title": str(finding["title"]),
"description": str(finding.get("description", "")),
"status": "failed",
"severity": str(finding.get("severity", "medium")),
"category": "TLS configuration",
"expected": "Current TLS protocol, cipher, and certificate configuration",
"observed": str(finding.get("evidence", ""))[-2000:],
"remediation": str(finding.get("remediation", "")),
"standards": standards(mapping, finding_id),
"evidence": [{"type": "text", "name": "TLS preflight", "value": str(finding.get("evidence", ""))[-2000:]}],
}
)
counts = dict.fromkeys(("passed", "failed", "errors", "skipped", "review"), 0)
counts["failed"] = len(results)
now = datetime.now(timezone.utc).isoformat()
report = {
"schema_version": "1.0.0",
"run": {
"id": os.environ.get("CI_PIPELINE_ID", now),
"started_at": now,
"source": "gitlab" if os.environ.get("CI") else "local",
"pipeline_url": os.environ.get("CI_PIPELINE_URL", ""),
"commit_sha": os.environ.get("CI_COMMIT_SHA", ""),
},
"project": {
"id": os.environ.get("TEST_PROJECT_ID", "demo-ubuntu-weak"),
"name": os.environ.get("TEST_PROJECT_NAME", "Ubuntu Weak Target Demonstration"),
"environment": os.environ.get("TEST_ENVIRONMENT", "test"),
"customer": os.environ.get("TEST_CUSTOMER", "Internal"),
"location": os.environ.get("TEST_LOCATION", "testserv"),
},
"tool": {"id": "zaproxy", "name": "OWASP ZAP with TLS preflight", "adapter_version": "1.0.0"},
"target": {"id": "demo-web", "type": "web_application", "address": args.target, "groups": ["web_applications"]},
"summary": {"total": len(results), **counts, "score": 0},
"results": results,
"raw_artifacts": [str(args.zap_json), str(args.tls_json)],
}
schema = json.loads(args.schema.read_text(encoding="utf-8"))
Draft202012Validator(schema, format_checker=FormatChecker()).validate(report)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
print(f"Normalized {len(results)} web findings")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Collect focused TLS evidence that ZAP baseline does not enumerate."""
import argparse
import json
import subprocess
from pathlib import Path
from urllib.parse import urlparse
def openssl_handshake(host: str, port: int, option: str, cipher: str | None = None) -> tuple[bool, str]:
command = ["openssl", "s_client", "-connect", f"{host}:{port}", "-servername", host, option, "-brief"]
if cipher:
command.extend(["-cipher", cipher])
result = subprocess.run(command, input="", text=True, capture_output=True, timeout=20, check=False)
evidence = (result.stdout + result.stderr).strip()
return result.returncode == 0 and "Protocol version" in evidence, evidence[-2000:]
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("url")
parser.add_argument("output", type=Path)
args = parser.parse_args()
parsed = urlparse(args.url)
host = parsed.hostname
port = parsed.port or 443
if not host:
parser.error("URL must include a hostname")
findings = []
for option, label in (("-tls1", "TLS 1.0"), ("-tls1_1", "TLS 1.1")):
accepted, evidence = openssl_handshake(host, port, option, "AES128-SHA:@SECLEVEL=0")
if accepted:
findings.append(
{
"id": "TLS-OLD-PROTOCOL",
"title": f"Server accepts {label}",
"severity": "high",
"description": "The endpoint accepts an obsolete TLS protocol.",
"remediation": "Allow only TLS 1.2 and TLS 1.3.",
"evidence": evidence,
}
)
weak_cipher, cipher_evidence = openssl_handshake(host, port, "-tls1_2", "AES128-SHA:@SECLEVEL=0")
if weak_cipher:
findings.append(
{
"id": "TLS-WEAK-CIPHER",
"title": "Server accepts TLS_RSA_WITH_AES_128_CBC_SHA",
"severity": "medium",
"description": "The endpoint accepts a legacy RSA/CBC cipher suite.",
"remediation": "Use forward-secret AEAD cipher suites.",
"evidence": cipher_evidence,
}
)
verification = subprocess.run(
[
"openssl",
"s_client",
"-connect",
f"{host}:{port}",
"-servername",
host,
"-verify_return_error",
"-brief",
],
input="",
text=True,
capture_output=True,
timeout=20,
check=False,
)
verification_evidence = (verification.stdout + verification.stderr).strip()
if verification.returncode != 0 and "certificate verify failed" in verification_evidence.lower():
findings.append(
{
"id": "TLS-SELF-SIGNED",
"title": "TLS certificate is not publicly trusted",
"severity": "medium",
"description": "Default certificate verification rejected the endpoint certificate.",
"remediation": "Install a certificate issued by a trusted CA for the environment.",
"evidence": verification_evidence[-2000:],
}
)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps({"target": args.url, "findings": findings}, indent=2) + "\n", encoding="utf-8")
print(f"Collected {len(findings)} TLS findings")
return 0
if __name__ == "__main__":
raise SystemExit(main())