Skip to content

Commit d146354

Browse files
Copilotandymeneely
andauthored
fix: harden ini config validation and overrides
Agent-Logs-Url: https://github.com/VulnerabilityHistoryProject/recidivism/sessions/a059b408-391c-467d-b495-6d9d3789015b Co-authored-by: andymeneely <341847+andymeneely@users.noreply.github.com>
1 parent 9e0bc9d commit d146354

4 files changed

Lines changed: 38 additions & 24 deletions

File tree

scripts/clone_osv_repositories.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from urllib.parse import urlparse
66

77
from osv_common import extract_repo_urls, iter_vulnerability_files, load_vulnerability
8-
from recidivism_config import load_config_with_source, required_value, resolve_config_path
8+
from recidivism_config import get_required_value, load_config_with_source, resolve_config_path
99

1010

1111
def clone_or_update(repo_url: str, target_dir: Path, update_existing: bool) -> None:
@@ -46,13 +46,18 @@ def main() -> None:
4646
config, config_source = load_config_with_source("clone")
4747

4848
parser = argparse.ArgumentParser(description="Clone all repositories referenced by OSV vulnerabilities.")
49-
parser.add_argument("--osv-dir", help="Directory containing extracted OSV JSON files")
50-
parser.add_argument("--target-dir", help="Directory to place local repository clones")
51-
max_repos_str = config.get("max_repos", fallback="").strip()
49+
parser.add_argument(
50+
"--osv-dir",
51+
help="Directory containing extracted OSV JSON files (overrides clone.osv_dir in recidivism.ini)",
52+
)
53+
parser.add_argument(
54+
"--target-dir",
55+
help="Directory to place local repository clones (overrides clone.target_dir in recidivism.ini)",
56+
)
5257
parser.add_argument(
5358
"--max-repos",
5459
type=int,
55-
default=int(max_repos_str) if max_repos_str else None,
60+
default=None,
5661
help="Optional limit for number of repositories",
5762
)
5863
parser.add_argument(
@@ -64,10 +69,18 @@ def main() -> None:
6469
args = parser.parse_args()
6570

6671
try:
67-
osv_dir = resolve_config_path(args.osv_dir or required_value(config, "osv_dir"))
68-
target_dir = resolve_config_path(args.target_dir or required_value(config, "target_dir"))
72+
osv_dir = resolve_config_path(args.osv_dir or get_required_value(config, "clone", "osv_dir"))
73+
target_dir = resolve_config_path(args.target_dir or get_required_value(config, "clone", "target_dir"))
6974
except ValueError as error:
7075
parser.error(f"{error} (config: {config_source})")
76+
max_repos = args.max_repos
77+
if max_repos is None:
78+
max_repos_str = config.get("max_repos", fallback="").strip()
79+
if max_repos_str:
80+
try:
81+
max_repos = int(max_repos_str)
82+
except ValueError as error:
83+
parser.error(f"Invalid clone.max_repos value '{max_repos_str}' in {config_source}: {error}")
7184
target_dir.mkdir(parents=True, exist_ok=True)
7285

7386
repo_urls = set()
@@ -76,8 +89,8 @@ def main() -> None:
7689
repo_urls.update(extract_repo_urls(vulnerability))
7790

7891
ordered_repos = sorted(repo_urls)
79-
if args.max_repos is not None:
80-
ordered_repos = ordered_repos[: args.max_repos]
92+
if max_repos is not None:
93+
ordered_repos = ordered_repos[:max_repos]
8194

8295
for repo_url in ordered_repos:
8396
clone_or_update(repo_url, target_dir, args.update_existing)

scripts/enrich_osv_recidivism.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from urllib.request import urlretrieve
99

1010
from osv_common import collect_history, iter_vulnerability_files, load_vulnerability, recidivism_for_vulnerability
11-
from recidivism_config import load_config_with_source, required_value, resolve_config_path
11+
from recidivism_config import get_required_value, load_config_with_source, resolve_config_path
1212

1313

1414
def download_dump(url: str, destination: Path, force: bool) -> None:
@@ -37,10 +37,10 @@ def main() -> None:
3737
config, config_source = load_config_with_source("enrich")
3838

3939
parser = argparse.ArgumentParser(description="Download OSV dump and enrich with recidivism metrics.")
40-
parser.add_argument("--dump-url")
41-
parser.add_argument("--archive-path")
42-
parser.add_argument("--extract-dir")
43-
parser.add_argument("--output")
40+
parser.add_argument("--dump-url", help="Override enrich.dump_url from recidivism.ini")
41+
parser.add_argument("--archive-path", help="Override enrich.archive_path from recidivism.ini")
42+
parser.add_argument("--extract-dir", help="Override enrich.extract_dir from recidivism.ini")
43+
parser.add_argument("--output", help="Override enrich.output from recidivism.ini")
4444
parser.add_argument(
4545
"--force-download",
4646
action=argparse.BooleanOptionalAction,
@@ -54,10 +54,10 @@ def main() -> None:
5454
args = parser.parse_args()
5555

5656
try:
57-
dump_url = args.dump_url or required_value(config, "dump_url")
58-
archive_path = resolve_config_path(args.archive_path or required_value(config, "archive_path"))
59-
extract_dir = resolve_config_path(args.extract_dir or required_value(config, "extract_dir"))
60-
output_path = resolve_config_path(args.output or required_value(config, "output"))
57+
dump_url = args.dump_url or get_required_value(config, "enrich", "dump_url")
58+
archive_path = resolve_config_path(args.archive_path or get_required_value(config, "enrich", "archive_path"))
59+
extract_dir = resolve_config_path(args.extract_dir or get_required_value(config, "enrich", "extract_dir"))
60+
output_path = resolve_config_path(args.output or get_required_value(config, "enrich", "output"))
6161
except ValueError as error:
6262
parser.error(f"{error} (config: {config_source})")
6363

scripts/recidivism_config.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,12 @@ def resolve_config_path(path_value: str) -> Path:
6565
return (REPO_ROOT / path).resolve()
6666

6767

68-
def required_value(config: configparser.SectionProxy, key: str) -> str:
68+
def get_required_value(config: configparser.SectionProxy, section: str, key: str) -> str:
6969
"""Return a required non-empty configuration value.
7070
7171
Args:
7272
config: Configuration section containing script settings.
73+
section: Section name for diagnostics.
7374
key: Config key to read.
7475
7576
Returns:
@@ -78,7 +79,7 @@ def required_value(config: configparser.SectionProxy, key: str) -> str:
7879
Raises:
7980
ValueError: If the key is missing or empty.
8081
"""
81-
value = config.get(key)
82+
value = config.get(key, fallback=None)
8283
if value is None or not value.strip():
83-
raise ValueError(f"Missing required config key '{key}' in section [{config.name}].")
84+
raise ValueError(f"Missing required config key '{key}' in section [{section}].")
8485
return value

tests/test_recidivism_config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88

99
sys.path.insert(0, str((Path(__file__).resolve().parents[1] / "scripts")))
1010

11-
from recidivism_config import required_value, load_config, resolve_config_path # noqa: E402
11+
from recidivism_config import get_required_value, load_config, resolve_config_path # noqa: E402
1212

1313

1414
class RecidivismConfigTests(unittest.TestCase):
15-
def test_loads_default_and_prints_message_when_local_missing(self) -> None:
15+
def test_fallback_to_default_when_local_missing(self) -> None:
1616
with tempfile.TemporaryDirectory() as tmp:
1717
tmp_path = Path(tmp)
1818
default_config = tmp_path / "recidivism.default.ini"
@@ -43,7 +43,7 @@ def test_required_value_raises_for_empty(self) -> None:
4343
with patch("recidivism_config.LOCAL_CONFIG_FILE", config_path):
4444
section = load_config("clone")
4545
with self.assertRaises(ValueError):
46-
required_value(section, "osv_dir")
46+
get_required_value(section, "clone", "osv_dir")
4747

4848

4949
if __name__ == "__main__":

0 commit comments

Comments
 (0)