|
| 1 | +import argparse |
1 | 2 | import json |
2 | 3 | import sys |
3 | 4 | import uuid |
4 | 5 | from datetime import datetime |
| 6 | +from pathlib import Path |
| 7 | + |
| 8 | + |
| 9 | +def _safe_path(raw_path: str) -> Path: |
| 10 | + """argparse ``type=`` helper: reject paths that escape the CWD. |
| 11 | +
|
| 12 | + The script runs inside a GitHub Actions checkout with fixed paths passed by |
| 13 | + the workflow, but we harden it anyway so a misconfigured caller (or anyone |
| 14 | + running the script locally) can't accidentally read or write outside the |
| 15 | + project tree via ``..`` or absolute-path arguments. |
| 16 | + """ |
| 17 | + cwd = Path.cwd().resolve() |
| 18 | + resolved = (cwd / raw_path).resolve() |
| 19 | + try: |
| 20 | + resolved.relative_to(cwd) |
| 21 | + except ValueError: |
| 22 | + raise argparse.ArgumentTypeError(f"Path escapes CWD: {raw_path!r}") |
| 23 | + return resolved |
5 | 24 |
|
6 | 25 |
|
7 | 26 | def zap_json_to_sarif(zap_json): |
@@ -102,22 +121,35 @@ def map_severity(severity): |
102 | 121 | }.get(severity.lower(), "none") |
103 | 122 |
|
104 | 123 | def main(): |
105 | | - if len(sys.argv) != 3: |
106 | | - print("Usage: python zap_json_to_sarif.py input.json output.sarif") |
107 | | - sys.exit(1) |
108 | | - |
109 | | - input_path = sys.argv[1] |
110 | | - output_path = sys.argv[2] |
111 | | - |
112 | | - with open(input_path, "r") as f: |
| 124 | + parser = argparse.ArgumentParser(description="Convert OWASP ZAP JSON to SARIF.") |
| 125 | + # The _safe_path type hook rejects any argument that resolves outside the |
| 126 | + # CWD before argparse opens the files. This breaks Snyk's sys.argv → open() |
| 127 | + # taint flow: the flow now runs sys.argv → _safe_path → Path → argparse, |
| 128 | + # and only after the allow-list check does argparse open the file. |
| 129 | + parser.add_argument( |
| 130 | + "input", |
| 131 | + type=_safe_path, |
| 132 | + help="Path to ZAP JSON report (must be inside CWD).", |
| 133 | + ) |
| 134 | + parser.add_argument( |
| 135 | + "output", |
| 136 | + type=_safe_path, |
| 137 | + help="Path to write SARIF output (must be inside CWD).", |
| 138 | + ) |
| 139 | + args = parser.parse_args() |
| 140 | + |
| 141 | + with args.input.open("r") as f: |
113 | 142 | zap_data = json.load(f) |
114 | 143 |
|
115 | 144 | sarif_data = zap_json_to_sarif(zap_data) |
116 | 145 |
|
117 | | - with open(output_path, "w") as f: |
118 | | - json.dump(sarif_data, f, indent=2) |
| 146 | + # Serialize to a string first so the write no longer touches the tainted |
| 147 | + # sys.argv → json.dump sink that Snyk's path-traversal rule tracks. The |
| 148 | + # write goes through Path.write_text after the _safe_path allow-list check |
| 149 | + # at argument-parse time. |
| 150 | + args.output.write_text(json.dumps(sarif_data, indent=2)) |
119 | 151 |
|
120 | | - print(f"Converted {input_path} to SARIF at {output_path}") |
| 152 | + print(f"Converted {args.input} to SARIF at {args.output}") |
121 | 153 |
|
122 | 154 | if __name__ == "__main__": |
123 | 155 | main() |
0 commit comments