Skip to content

Commit 37938fc

Browse files
authored
Merge pull request #921 from MISP/fix/vulnerability-lookup-null-settings-20261013
Fix vulnerability_lookup defaults for unset MISP settings
2 parents b1c4eae + 50d5c0a commit 37938fc

2 files changed

Lines changed: 71 additions & 0 deletions

File tree

‎misp_modules/modules/expansion/vulnerability_lookup.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
"it bounds alias lookup attempts, CPE results and each enrichment section. "
4040
"with_linked, with_comments, with_bundles, with_sightings, with_meta, with_epss and with_kev default to true; "
4141
"with_timeline defaults to false. Boolean options accept true/false or their string equivalents.\n"
42+
"Unset options (null or blank strings from MISP) use their documented defaults. "
43+
"The request's top-level timeout limits total module execution independently of config.timeout.\n"
4244
"CPE lookup uses cpe_source (default cvelistv5) and cpe_page (default 1). Results match vendor and product; "
4345
"exact version applicability must be checked. CPE search imports advisories from one bounded page; "
4446
"the optional community and priority lookups apply to vulnerability ID input.\n"
@@ -80,6 +82,13 @@ def handler(q=False):
8082
config = request.get("config") or {}
8183
if not isinstance(config, dict):
8284
return {"error": "Invalid module configuration."}
85+
# MISP sends all advertised settings, including null/blank values for unset options.
86+
# Keep explicit false and zero; they are meaningful configuration values.
87+
config = {
88+
key: value
89+
for key, value in config.items()
90+
if value is not None and not (isinstance(value, str) and not value.strip())
91+
}
8392
try:
8493
timeout = float(config.get("timeout", 30))
8594
max_related = int(config.get("max_related", 50))

‎tests/test_vulnerability_lookup.py‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,68 @@ def test_custom_url_timeout_and_identifier_encoding_are_used_for_all_requests():
532532
assert objects(result, "vulnerability")
533533

534534

535+
def test_misp_null_configuration_reaches_the_api_and_keeps_default_enrichment():
536+
query = {
537+
"module": "vulnerability_lookup",
538+
"attribute": {"type": "vulnerability", "value": CVE, "uuid": UUID},
539+
"config": dict.fromkeys(vulnerability_lookup.moduleconfig),
540+
"timeout": 10,
541+
}
542+
query["config"]["timeout"] = "80"
543+
with patch("requests.get", side_effect=[response(cve()), response({}, 404), response({}, 404)]) as get:
544+
result = vulnerability_lookup.handler(json.dumps(query))
545+
assert objects(result, "vulnerability")
546+
assert get.call_count == 3
547+
main_call = get.call_args_list[0]
548+
assert main_call.kwargs["timeout"] == 80
549+
assert main_call.kwargs["params"] == {
550+
"with_linked": "true",
551+
"with_comments": "true",
552+
"with_bundles": "true",
553+
"with_sightings": "true",
554+
"with_meta": "true",
555+
}
556+
assert get.call_args_list[-1].kwargs["params"]["per_page"] == 50
557+
assert not any(call.args[0].endswith("/timeline") for call in get.call_args_list)
558+
559+
560+
@pytest.mark.parametrize("unset", [None, "", " "])
561+
def test_unset_numeric_and_boolean_options_use_defaults(unset):
562+
config = dict.fromkeys(vulnerability_lookup.moduleconfig, unset)
563+
_, get = run(cve(), config=config, extra_responses=[response({}, 404), response({}, 404)])
564+
assert get.call_args_list[0].kwargs["timeout"] == 30
565+
assert get.call_args_list[0].kwargs["params"]["with_linked"] == "true"
566+
assert get.call_args_list[-1].kwargs["params"]["per_page"] == 50
567+
568+
569+
@pytest.mark.parametrize("unset", [None, "", " "])
570+
def test_unset_cpe_settings_use_defaults(unset):
571+
config = dict.fromkeys(vulnerability_lookup.moduleconfig, unset)
572+
result, get = run(
573+
{"cvelistv5": [cve()]}, attribute_type="cpe", value="cpe:2.3:a:example:app:*:*:*:*:*:*:*:*", config=config
574+
)
575+
assert objects(result, "vulnerability")
576+
assert get.call_args.kwargs["params"] == {"per_page": 50, "page": 1, "source": "cvelistv5"}
577+
578+
579+
@pytest.mark.parametrize("disabled", [False, "false", 0, "0"])
580+
def test_explicit_false_flags_are_not_replaced_by_defaults(disabled):
581+
config = dict.fromkeys(vulnerability_lookup.moduleconfig)
582+
config.update({name: disabled for name in vulnerability_lookup.moduleconfig if name.startswith("with_")})
583+
_, get = run(cve(), config=config)
584+
assert get.call_count == 1
585+
assert all(value == "false" for value in get.call_args.kwargs["params"].values())
586+
587+
588+
@pytest.mark.parametrize("zero", [0, "0"])
589+
def test_explicit_zero_related_limit_is_preserved(zero):
590+
config = dict.fromkeys(vulnerability_lookup.moduleconfig)
591+
config.update({"max_related": zero, "with_epss": False})
592+
result, get = run(cve(), config=config)
593+
assert objects(result, "vulnerability")
594+
assert get.call_count == 1
595+
596+
535597
@pytest.mark.parametrize("query", ["invalid", "[]", "null", "{}", '{"attribute": []}'])
536598
def test_invalid_input_is_an_error(query):
537599
with patch("requests.get") as get:

0 commit comments

Comments
 (0)