Summary
XeolParser derives a finding's severity from the wall clock at parse time, so parsing the same, unchanged report on two different days can produce findings with different severities. Parser output should be a pure function of its input; here it is a function of the input and the calendar.
Where
dojo/tools/xeol/parser.py, lines 66-83:
# Determine severity based on EOL date
severity = "Info"
eol_str = cycle.get("Eol", "")
try:
eol_date = datetime.strptime(eol_str, "%Y-%m-%d")
now = datetime.now()
if eol_date < now:
delta = now - eol_date
if delta <= timedelta(weeks=2):
severity = "Low"
elif delta <= timedelta(weeks=4):
severity = "Medium"
elif delta <= timedelta(weeks=6):
severity = "High"
else:
severity = "Critical"
except Exception:
severity = "Info"
now is the only non-input in the expression, and it moves.
Impact
A component that reached end of life walks through four severities over the six weeks that follow, with no change whatsoever to the scan report:
| Time since the component's EOL date |
Severity |
| not yet EOL |
Info |
| 0 to 2 weeks |
Low |
| 2 to 4 weeks |
Medium |
| 4 to 6 weeks |
High |
| beyond 6 weeks |
Critical (terminal) |
Two consequences:
- Severity churn on reimport. Re-importing an unchanged Xeol report re-grades existing findings. A user watching a dashboard sees EOL findings silently escalate with no upstream event behind the change, and any severity-based SLA or report shifts with it.
- Duplicate findings for anyone who hashes severity.
severity is a legal entry in HASHCODE_FIELDS_PER_SCANNER and is used by a number of scan types today (Anchore, Aqua, Burp and others). Xeol is not in that dict, so it currently falls back to the legacy field set (title, cwe, line, file_path, description) and its hash_code is unaffected. But a user who customizes the hash fields for Xeol to include severity, which is a supported configuration, gets a hash that changes on its own schedule: reimport stops matching the stored findings, closes them as absent, and creates duplicates. Rehashing cannot repair that, because the stored rows do not contain the values the parser would write today.
Reproduction
unittests/scans/xeol/xeol_one_finding.json carries "Eol": "2026-07-02" for Perl. That sample crossed the six-week boundary at 2026-08-13T00:00 local time of whatever runs the parser:
parse on 2026-08-12 -> severity High
parse on 2026-08-14 -> severity Critical
Nothing about the file changed.
Note this is also a latent trap in the test corpus: any assertion on that file's severity is time-dependent. unittests/tools/test_xeol_parser.py happens not to assert severity for xeol_one_finding.json (it only asserts it for xeol_multiple_findings.json, whose EOL dates are in 2019 and therefore permanently Critical), which is why CI has not surfaced this.
Prior art
This is the same class of defect as the Govulncheck parser building its description from ", ".join(set(...)), where Python's per-process string hash randomization made the description, and therefore the hash_code, differ on every import of the same file. That was fixed by sorting the sets in dojo/tools/govulncheck/parser.py. The principle applied there is the one at stake here: a parser's output must be reproducible from its input alone.
Possible fixes
- Grade against a timestamp carried in the report rather than
datetime.now(), so the result is fixed per file. The xeol JSON descriptor block does not appear to carry a scan timestamp today, so this may need an upstream tool change.
- Drop the time banding and assign a single severity to any past-EOL component (with
Info retained for one not yet EOL). Deterministic, and simple to reason about. This does change severity for existing findings on the next import, so it is worth calling out in release notes.
- Keep the banding but compute it from data in the report, for example the EOL date relative to the artifact's release or latest-release date, both of which are already parsed into the description.
Option 2 is the smallest change that makes the parser deterministic. Happy to open a PR for whichever direction maintainers prefer, since this is a product decision about how EOL findings should be graded rather than a purely mechanical fix.
Summary
XeolParserderives a finding'sseverityfrom the wall clock at parse time, so parsing the same, unchanged report on two different days can produce findings with different severities. Parser output should be a pure function of its input; here it is a function of the input and the calendar.Where
dojo/tools/xeol/parser.py, lines 66-83:nowis the only non-input in the expression, and it moves.Impact
A component that reached end of life walks through four severities over the six weeks that follow, with no change whatsoever to the scan report:
Two consequences:
severityis a legal entry inHASHCODE_FIELDS_PER_SCANNERand is used by a number of scan types today (Anchore, Aqua, Burp and others). Xeol is not in that dict, so it currently falls back to the legacy field set (title,cwe,line,file_path,description) and itshash_codeis unaffected. But a user who customizes the hash fields for Xeol to includeseverity, which is a supported configuration, gets a hash that changes on its own schedule: reimport stops matching the stored findings, closes them as absent, and creates duplicates. Rehashing cannot repair that, because the stored rows do not contain the values the parser would write today.Reproduction
unittests/scans/xeol/xeol_one_finding.jsoncarries"Eol": "2026-07-02"for Perl. That sample crossed the six-week boundary at2026-08-13T00:00local time of whatever runs the parser:Nothing about the file changed.
Note this is also a latent trap in the test corpus: any assertion on that file's severity is time-dependent.
unittests/tools/test_xeol_parser.pyhappens not to assert severity forxeol_one_finding.json(it only asserts it forxeol_multiple_findings.json, whose EOL dates are in 2019 and therefore permanently Critical), which is why CI has not surfaced this.Prior art
This is the same class of defect as the Govulncheck parser building its description from
", ".join(set(...)), where Python's per-process string hash randomization made the description, and therefore thehash_code, differ on every import of the same file. That was fixed by sorting the sets indojo/tools/govulncheck/parser.py. The principle applied there is the one at stake here: a parser's output must be reproducible from its input alone.Possible fixes
datetime.now(), so the result is fixed per file. The xeol JSONdescriptorblock does not appear to carry a scan timestamp today, so this may need an upstream tool change.Inforetained for one not yet EOL). Deterministic, and simple to reason about. This does change severity for existing findings on the next import, so it is worth calling out in release notes.Option 2 is the smallest change that makes the parser deterministic. Happy to open a PR for whichever direction maintainers prefer, since this is a product decision about how EOL findings should be graded rather than a purely mechanical fix.