CYBER-0 initial concept ready
This commit is contained in:
@@ -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
|
||||
```
|
||||
@@ -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"]
|
||||
}
|
||||
Submodule
+1
Submodule methodologies/zap/reference/OWASP_ASVS added at 2b300716eb
@@ -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
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user