Skip to content

fix(ui): correct #toggleBox positioning on login page in Chrome - #15769

Closed
koushik-hs wants to merge 126 commits into
DefectDojo:masterfrom
koushik-hs:fix/parser-edge-case
Closed

fix(ui): correct #toggleBox positioning on login page in Chrome#15769
koushik-hs wants to merge 126 commits into
DefectDojo:masterfrom
koushik-hs:fix/parser-edge-case

Conversation

@koushik-hs

@koushik-hs koushik-hs commented Aug 23, 2026

Copy link
Copy Markdown

Description

Renamed #toggleBox to #loginTogglePassword across login.html, index.js, and dojo.css. This resolves a CSS ID collision where the password visibility toggle button inherited global absolute positioning, causing layout misalignment in Google Chrome.

Fixes #15134

Test results

  • Verified locally using Docker Compose (docker compose up).
  • Confirmed layout alignment in Google Chrome: the toggle button now renders inline directly below the password input field.
  • Verified interactive click behavior toggles password visibility and swaps the eye icon/label correctly.

Screenshots

image

Checklist

  • Give a meaningful name to your PR.
  • Bugfixes should be submitted against the bugfix or dev branch.
  • Code is clean and strictly scoped to the targeted issue.

DefectDojo release bot and others added 30 commits August 3, 2026 16:09
….2.0-3.3.0-dev

Release: Merge back 3.2.0 into dev from: master-into-dev/3.2.0-3.3.0-dev
DefectDojo#15482)

* feat(parsers): add Socket and Lacework parsers

Both mirror the mapping the corresponding DefectDojo Pro connector already
implements, so that a file import and an API sync produce findings that
deduplicate against each other rather than two copies of everything. The scan
type strings are byte-identical to what each connector reports, and the
deduplication settings are copied from the connector's own configuration.

Socket: one finding per alert, identity is the Socket alert key. Note Socket
grades alerts low/middle/high/critical - "middle", not "medium"; guessing
"medium" would silently drop every middle-severity alert to Info.

Lacework: two shapes with different mappings. Container/image rows are static
findings keyed by image; host rows are dynamic findings keyed by hostname,
falling back to the machine id. Container rows report fix_available as an
integer and host rows as a string, so testing one shape's type against the other
would drop every host mitigation. A row Lacework reports as fixed or resolved is
imported but not active.

Fixtures are constructed from each vendor's documented schema with generic
package scopes, registries and hostnames.

* feat(parsers): add CrowdStrike Falcon Spotlight parser

Mirrors pkg/tools/crowdstrike/connector/vuln_converter.go field for field, and
uses the connector's ScanTypeSpotlight string verbatim, so a file import and an
API sync deduplicate against each other instead of producing two copies of
every finding. Dedup settings are copied from the connector's own config -
including the fact that it lists unique_id_from_tool among the hash fields as
well as pairing with unique_id_from_tool_or_hash_code.

Deliberately does NOT claim the connector's separate
"CrowdStrike:Detections - Connectors Import" scan type, which is a different
shape; a test asserts that.

Points worth noting, all mirrored rather than corrected:
- The connector marks these findings NEITHER static NOR dynamic. Spotlight reads
  the Falcon agent's software inventory: it neither analyses source nor probes a
  running service.
- Severity comes from the CVE, not the vulnerability.
- CrowdStrike returns no discrete version field, so component_version is what
  remains of product_name_version once the normalized product name is stripped.
- firstCWE takes the first entry parseable as CWE-<number>; a list with none
  parseable leaves cwe at 0, which is the IntegerField default rather than None.

The host is recorded via the nmap locations idiom (LocationData under
V3_FEATURE_LOCATIONS, Endpoint otherwise) rather than the connector's
protocol-relative "//<host>" string, which exists only to survive DefectDojo's
URI parsing. Tested through get_unsaved_locations so it passes in both modes.

Fixtures are constructed from the documented combined-vulnerabilities schema
with generic hostnames, private-range addresses and placeholder CVE ids.

* feat(parsers): add FOSSA parser

Mirrors pkg/tools/fossa/converter field for field, uses the connector's ScanType
string verbatim, and copies its dedup settings, so a file import and an API sync
deduplicate against each other.

Covers both of FOSSA's issue categories - security vulnerabilities and licensing
or quality issues - because the connector does.

Three points that needed care:

- An issue counts as a vulnerability when its type says so OR when it carries
  vulnerability-only fields (cve, vulnId, cvssVector). That belt-and-braces
  check is the converter's: a missing or renamed type must not silently
  downgrade a CVE to a licensing finding, which would also change its grading.
- Severity for a vulnerability is FOSSA's own, but FOSSA reports "unknown"
  often enough that the CVSS v3 band fallback matters. Licensing and quality
  issues carry no severity at all, so the connector's type table is mirrored -
  including BOTH spellings of the risk_* types, since FOSSA's docs hyphenate
  (risk_empty-package) while fossa-cli's wire format uses underscores.
- unique_id_from_tool is "<issue id>:<project locator>". One FOSSA issue can
  affect several projects and becomes one finding per DefectDojo product; those
  must not share a tool id. The parser reproduces this from the issue's own
  projects list, and falls back to the issue id alone when an export carries no
  project context - documented, since that case cannot reproduce the suffix.

FOSSA is SCA, so a finding has no file or line; the dependency coordinates are
its only location and are written into the description.

References are accepted as either a bare string or a {url, title} object, which
is what the connector's Reference.Link() handles.

* feat(parsers): add Endor Labs parser

Mirrors pkg/tools/endorlabs/connector/converter.go field for field, uses the
connector's ScanType string verbatim, and copies its dedup settings, so a file
import and an API sync deduplicate against each other.

Endor Labs is reachability-aware SCA, and its reachability verdict is the one
thing that distinguishes it from any other SCA tool. The connector promotes that
verdict to the finding's IMPACT rather than leaving it in tags, and this does the
same. The precedence is the connector's: a function-level verdict outranks a
dependency-level one, and a definite verdict outranks a "potentially".

Endor advisory text arrives as HTML from upstream advisories, and the connector
never renders it. Its InertText is reproduced here: script and style content is
dropped, block tags become newlines, runs of blank lines collapse, and the result
is escaped. Go's html.EscapeString is matched byte for byte rather than using
Python's html.escape, which spells the apostrophe entity differently.

Other points mirrored rather than corrected:
- CVSS prefers the v3 score but falls back to the v4 base score; without that a
  v4-only advisory would import with no score at all.
- Vulnerability IDs are the primary identifier followed by Endor's aliases,
  deduplicated, so a GHSA and its CVE both land on the finding.
- Component name falls back from the resolved dependency name to the package name.
- Tags drop any value ending _UNSPECIFIED: those only record that Endor did not
  determine something.
- Identity is the finding UUID, falling back to a vulnerability/component
  composite when Endor sends none.
- Endor also reports secrets and other non-CVE findings with no vulnerability
  block at all; those import without CVE, score or reachability.

Fixtures are constructed from Endor's documented findings schema with generic
package names and a generic tenant namespace.

* feat(parsers): add GitGuardian parser

Mirrors pkg/tools/gitguardian/connector/converter.go field for field, uses the
connector's ScanTypeName verbatim, and copies its dedup settings, so a file
import and an API sync deduplicate against each other. Note GitGuardian pairs
the PLAIN hash_code algorithm with a unique-id-only hash field set, unlike the
other connector scan types added here - incident ids are stable, so nothing else
is needed.

One finding per incident, not per occurrence: an incident is one distinct
exposed credential however many times it appears, and the occurrence count goes
in the description.

No secret value is imported. GitGuardian's incidents endpoint does not return the
matched secret, and a test asserts that, so a future change that starts pulling
occurrences cannot quietly begin copying credentials into the database.

GitGuardian actively checks whether a credential still authenticates, which is
the most actionable thing it reports. The connector spells that verdict out
rather than leaving a bare enum, and marks the finding verified ONLY for
"valid" - an unchecked credential is not evidence either way, and marking it
verified would overstate what GitGuardian knows. The three unverified states
(no_checker, not_checked, failed_to_check) share one narrative.

Fixtures are constructed from the documented secret-incident schema with generic
incident names and a generic dashboard host; none contains a credential-shaped
value.

* feat(parsers): add Codacy parser

Mirrors pkg/tools/codacy/converter field for field, uses the connector's ScanType
string verbatim, and copies its dedup settings, so a file import and an API sync
deduplicate against each other.

Codacy surfaces several underlying scanners through one security-items endpoint,
so the parser flags a finding static or dynamic from the item's own scanType -
only DAST looked at something running. Flagging all of them one way would
misreport most.

Points that needed care, all mirrored from the converter:
- The vulnerable package is the LAST entry of the first non-empty dependency
  chain. The first entry is the project itself, so taking it would name the
  application as the vulnerable component on every SCA finding. A leading empty
  chain is skipped rather than treated as "no package".
- Codacy's "cve" is a typed string documented as possibly holding several
  identifiers, so it is scanned for all of them and deduplicated;
  vuln_id_from_tool takes the first and falls back to Codacy's itemSourceId.
- An item Codacy ignored as a FALSE POSITIVE imports with false_p set, so
  triaged noise does not go back in front of the team. Any other ignore reason
  ("acceptable risk") is a real finding somebody accepted and is NOT flagged.
  The reason comparison strips spaces so "False Positive" matches.
- The converter dates a finding today when openedAt will not parse, so a finding
  always has a date. Mirrored; the test asserts a range so it cannot flake on a
  date rollover.

The scanned application (or, for a container item, its image) is recorded via the
locations idiom so it survives under either value of V3_FEATURE_LOCATIONS.

* feat(parsers): add DeepSource parser

Mirrors pkg/tools/deepsource/converter field for field, uses the connector's
ScanType string verbatim, and copies its dedup settings, so a file import and an
API sync deduplicate against each other.

DeepSource reports two different things and the connector converts them
differently, so both shapes are handled: static analysis issue occurrences, and
dependency vulnerabilities from advisories. A bare array is classified per entry
rather than per file, so a mixed export is not mis-mapped.

The severity work is the interesting part. DeepSource grades EVERY issue
CRITICAL/MAJOR/MINOR whatever the issue actually is - a missing docstring can be
MAJOR - so the category decides which ladder applies. A security issue keeps its
grade; a bug-risk, performance, typecheck or anti-pattern issue drops a step,
because it describes a defect rather than a weakness. Applying one ladder to both
would either inflate every lint finding or bury the real ones. A hit from the
secrets analyzer is Critical regardless, since a committed credential is a
committed credential.

Advisories use a separate ladder: the CVSS v3 band when scored (bottoming out at
Low, never Info), otherwise the severity word, accepting GitHub's MODERATE
spelling of medium. Identifiers are the advisory id plus aliases, upper-cased and
deduplicated. An advisory with no published fix says so explicitly rather than
leaving the mitigation blank - "no fix published" is useful triage information.

Note the connector grades STYLE, DOCUMENTATION and COVERAGE issues as Info and
imports them. That is mirrored here rather than filtered, because parity with the
connector is what stops findings duplicating; it is raised in the PR as a
follow-up worth discussing against the connector instead.

One bug caught by the clean-run fixture: presence of the occurrences or
vulnerabilities KEY identifies the shape, not whether it has entries - a clean
run legitimately reports both lists empty.

* feat(parsers): add Probely parser

Mirrors pkg/tools/probely/connector/finding_converter.go field for field and uses
the connector's ScanType verbatim. That string is "Probely API Import" - it does
NOT follow the "<Vendor> - Connectors Import" pattern the other connector scan
types use, so it cannot be derived from the vendor name and a test asserts it.

Probely reports severity as an INTEGER with only three values (10/20/30 ->
Low/Medium/High, no Critical). Treating the number as a score or an index would
misgrade every finding.

Findings Probely records as fixed, invalid or accepted are skipped, matching the
converter's IsIgnored. "retesting" is deliberately NOT skipped: a re-test means
somebody is actively working the issue, so it is assumed still open. Skipping it
would drop live findings whenever a re-test was queued.

This scan type's dedup config pairs the plain hash_code algorithm with a wide
field set that includes ENDPOINTS, so the parser always records the scanned
origin - reduced to scheme, host and port as the converter does. An unpopulated
endpoint would leave the hash computed over nothing and every rescan would
reimport. Recorded via the locations idiom so it works in both
V3_FEATURE_LOCATIONS modes, and a test pairs the assertion with the dedup field
list so the two cannot drift apart.

One parity bug caught by the field-mapping test: Probely names the definition's
prose field "desc", not "description", so reading the latter silently produced
an empty description.

The insertion point is rendered as a readable label with the converter's acronym
fixes (URL/JSON/GraphQL); plain title casing gives "Url" and "Json".

* feat(parsers): add Detectify parser

Mirrors pkg/tools/detectify/connector/finding_converter.go field for field and
uses the connector's ScanType verbatim. Like Probely, that string is
"Detectify Scan" - it does NOT follow the "<Vendor> - Connectors Import"
pattern, so it cannot be derived; a test asserts it.

Findings Detectify records as patched or false_positive are skipped, but an
ACCEPTED RISK is deliberately kept and flagged risk_accepted rather than
discarded - dropping it would lose the record that somebody accepted it.

Detectify reports separate CVSS 2.0, 3.0 and 3.1 blocks. The converter prefers
3.1, falls back to 3.0, and ignores 2.0 because cvssv3 is a v3 field. A block
counts as present when it carries a score OR a vector, so a vector-only entry is
not discarded; the fixture gives all three blocks different values to prove the
preference.

Detectify has no dedicated CVE field, so identifiers are extracted from the
finding title, the definition's title, description and risk text, and every
reference name and link, then deduplicated in order.

Endpoint preference is the converter's: the request URL, then the host with the
location appended ONLY when it starts with "/" (otherwise it is not a path and
concatenating would produce a nonsense host), then the location alone. Recorded
via the locations idiom so it works in both V3_FEATURE_LOCATIONS modes.

Detectify supplies no remediation prose, only reference links, so the mitigation
points at them rather than being left empty. CWE arrives as a plain integer, not
a CWE-<n> string.

* feat(parsers): add HackerOne and YesWeHack parsers

Two more bug-bounty platforms, same rule as the rest of this PR: the scan-type
string is byte-identical to the connector's and the dedup config is copied from
the connector's own block. Both use the plain hash_code algorithm over
unique_id_from_tool alone, because report ids are globally unique on each
platform.

HackerOne's API is JSON:API, and severity, weakness and reporter are
RELATIONSHIPS rather than attributes - each nested under
relationships.<name>.data.attributes. Reading them off the top level would leave
every finding at Info with no CWE and no reporter, silently and with no error, so
a test asserts the raw fixture really does not carry them anywhere else. An
already-flattened export is accepted too.

YesWeHack's workflow state carries real triage information and the converter
translates each one rather than importing everything active: accepted ->
active+verified, resolved/auto_close -> mitigated, wont_fix -> risk accepted,
invalid/rejected -> false positive, duplicate -> duplicate,
out_of_scope/informative -> inactive. An unrecognised state stays ACTIVE so a
state YesWeHack adds later cannot silently close a finding. Its severity resolves
through three sources in order - CVSS criticity, priority name, priority slug -
because the criticity is often empty while a priority is set.

* feat(parsers): add Intigriti parser

Mirrors pkg/tools/intigriti/connector/finding_converter.go field for field, uses
the connector's ScanType verbatim, and copies its dedup settings (plain hash_code
over unique_id_from_tool alone - submission codes are globally unique).

Three things needed care:

- Intigriti's API lists submissions and serves each report separately, so the
  converter builds a finding from TWO objects and prefers the overview wherever
  both carry a field. An export may nest the detail under "detail" or carry the
  report on the entry itself; both are recognised, because missing the merged
  form would lose the CWE, impact, solution and the whole description body.
- Intigriti grades its top tier "Exceptional", not "Critical". Mapping only
  "critical" would silently drop every top-tier submission to Info.
- For a closed or archived submission the CLOSE REASON distinguishes a fix from a
  rejection: accepted risk -> risk accepted, duplicate -> duplicate, out of scope
  -> out of scope, and the rejection reasons (including Intigriti's terse "no")
  -> false positive, with anything else treated as fixed. Treating every closed
  submission the same way would mark rejected and duplicate submissions as
  mitigated, which reads as work completed.

Researcher-submitted prose - proof of concept, impact, solution, asset and the
intake question answers - is flattened to escaped plain text rather than
rendered, matching the connector's InertText including Go's html.EscapeString
entities. The connectors repo duplicates that sanitizer per tool rather than
sharing it, so it is reproduced here rather than imported from another parser.

* feat(parsers): add Bugcrowd connector-parity parser

Mirrors pkg/tools/bugcrowd/connector/converter.go field for field, uses the
connector's ScanTypeName verbatim, and copies its dedup settings (plain hash_code
over unique_id_from_tool alone).

Deliberately a SEPARATE directory from the shipped `bugcrowd` parser, which
handles Bugcrowd's CSV export under the scan type "BugCrowd Scan" (capital C).
Two formats, two scan types; a test asserts both so neither can start shadowing
the other, and the CSV parser is untouched.

Behaviour mirrored rather than chosen:
- "triaging" is NOT importable. A submission mid-triage has no confirmed verdict,
  so importing it would put unvetted researcher claims into the queue.
- not_applicable overrides the priority as well as closing the finding: a P1 that
  Bugcrowd then judged not applicable must not sit in the queue as Critical.
- informational is imported but inactive, so a courtesy report is recorded
  without occupying the open queue.
- P5 has no mapping and lands at Info, like anything unrecognised.
- Titles are researcher-written, so colons and quotes become spaces and "@"
  becomes "at" - but only when the title needs it; whitespace collapses and an
  over-long title is cut to DefectDojo's 511-character column with an ellipsis.
- States are normalised, so Bugcrowd's hyphenated and underscored spellings both
  match the importable set instead of being silently dropped.
- A schemeless bug_url is prefixed with "//" so the host survives URI parsing.

One faithfulness note: with no programme code in the export the tracker link
contains a double slash, because the connector concatenates base + code + self
link. Reproduced rather than tidied, and the test says why - the connector always
has a code, so tidying here would be the only divergence for an export that
carries one.

* feat(parsers): add Cobalt.io connector-parity parser

Mirrors pkg/tools/cobalt/connector/converter.go field for field, uses the
connector's ScanTypeName verbatim, and copies its dedup settings (plain hash_code
over unique_id_from_tool alone).

A SEPARATE directory from the shipped `cobalt` parser, which handles Cobalt's CSV
export under "Cobalt.io Scan". Two formats, two scan types; a test asserts both
and the CSV parser is untouched.

Three details that would each be wrong if guessed:

- Cobalt nests each finding under "resource" but puts the human-facing deep link
  OUTSIDE it at links.ui.url. Reading the entry directly finds no fields; reading
  only the resource loses the link, which is the only route back to the pentest
  report. A test asserts the raw fixture really is shaped that way.
- The date comes from the "created" entry in the finding's LOG, not created_at.
  Cobalt can carry a finding over from an earlier pentest, and then created_at is
  the carry-over date - the fixture has one created in January and carried over in
  July, so taking created_at would date it six months late.
- CVSS takes the first entry whose version starts with 3, because Cobalt reports
  v2 and v3 side by side and cvssv3 is a v3 field. The fixture lists the v2 entry
  FIRST so a naive first-match would be caught.

State handling is the connector's: only valid_fix, invalid and out_of_scope close
a finding, so a DUPLICATE or an ACCEPTED RISK stays active and is merely flagged.
new and triaging are the only unverified states. A state Cobalt adds later is not
imported at all - every documented state is already in the importable set, so an
unknown one means the API changed and skipping beats guessing.

Impact and likelihood are numeric and ZERO IS A REAL SCORE, so presence is tested
against None and the empty string rather than truthiness; ruff suggested the
falsy shortcut here and it would have silently dropped the zero.

* feat(parsers): add Harbor connector-parity parser

Mirrors pkg/tools/harbor/converter field for field, uses the connector's ScanType
verbatim, and copies its dedup settings (plain hash_code over unique_id_from_tool
alone - the composed id already carries repository, artifact, vulnerability and
package).

Own directory, distinct from the shipped `harbor_vulnerability` parser
("Harbor Vulnerability Scan"); a test asserts both scan types differ.

Two things a reader would not guess:

- Harbor's scan endpoint keys the report by the SCANNER'S MIME TYPE, so a saved
  export is normally an object whose single key is
  "application/vnd.security.vulnerability.report; version=1.1". Expecting the
  bare report would reject every real export, so the envelope is unwrapped.
- The artifact's identity is NOT in the report body at all. The connector supplies
  repository, tag and digest from the artifact it fetched, and they feed both the
  finding identity and the image context in the description. An export should
  carry them; without them the finding still imports with empty segments, which is
  the connector's own behaviour when the fields are blank. The digest is preferred
  over the tag because a tag can be moved to a different image, which would
  silently merge findings from two artifacts.

Also mirrored: only a CVE id becomes a vulnerability id (Harbor reports GHSA and
distro ids too, and a GHSA in the CVE field would have DefectDojo try to resolve
it as one), and a missing description becomes "No description found" rather than
an empty field that reads as a parser bug.

* fix(parsers): drop duplicate-vendor parsers, fix DeepSource input shape

Two corrections, both from review.

1. A parser is scoped to ONE uploaded run; a connector syncs an entire account's
   ongoing state. So a second parser for a vendor that already has one is
   redundant. Dropped bugcrowd_connectors, cobalt_connectors and
   harbor_connectors - DefectDojo already ships bugcrowd (CSV), cobalt (CSV) and
   harbor_vulnerability, and those cover the file-import case. Their settings
   entries are reverted too.

   That also removes the export conventions I had invented purely to fake
   whole-account context a single-run file does not have: Harbor's
   repository/tag/digest and Bugcrowd's program_code.

   Quay keeps its parser - DefectDojo has none for Quay - and takes the plain
   `quay` directory rather than a _connectors suffix.

2. DeepSource has NO REST API; everything goes through POST /graphql/. The
   envelope the parser accepted ({"run":..., "occurrences": [...]}) mirrored the
   connector's internal Go model and was NOT something a DeepSource user could
   produce, so the parser could not do its job. It now reads the real saved
   GraphQL response - data.repository with each collection behind a GraphQL
   connection (edges[].node) - and the fixtures are that shape. A test pins the
   envelope so it cannot regress to the invented one.

   Only the wrapper was wrong: every field mapping already used DeepSource's
   camelCase names (cvssV3BaseScore, epssScore, beginLine), and 26 of 27 existing
   tests passed unchanged against the real envelope.

14 parsers remain, one per vendor, none duplicating a shipped parser.

* feat(parsers): add Google Cloud SCC parser

Mirrors pkg/tools/googlescc/connector/converter.go field for field, uses the
connector's ScanTypeName verbatim, and copies its dedup settings (plain hash_code
over unique_id_from_tool alone - the finding's full resource name is globally
unique across the organisation).

The shape detail worth knowing: SCC's ListFindings pairs each finding with the
resource it was found on as SIBLINGS, not nested. Both halves matter - the
category and severity are on the finding, while the display name and type that
make it readable are on the resource. Reading the result as if it were the finding
would import nothing; reading only the finding half would lose the resource
context. A test asserts the raw fixture really is shaped that way.

Also mirrored: the title falls back from "<category> - <resource>" to the
category alone and then to a constant, because SCC does not always set a category
and an empty title is useless in the finding list; SEVERITY_UNSPECIFIED falls
through to Info; and CVE/CVSS live two objects deep under vulnerability.cve and
exist only on vulnerability-class findings, so a misconfiguration or observation
finding legitimately has neither. The score is recorded only when above zero.

First of the vendors that have no DefectDojo parser of any kind.

* feat(parsers): add Fairwinds Insights parser

Mirrors pkg/tools/fairwinds/converter field for field, uses the connector's
ScanType verbatim, and copies its dedup settings.

Two things that would be wrong if guessed:

- Fairwinds normalises severity to a 0.0-1.0 FLOAT, not a word and not a CVSS
  score. The breakpoints are its own (0.9/0.7/0.4/0.1); reading the number as
  CVSS would put every finding at Info. A malformed value falls to Info rather
  than erroring.
- Fairwinds aggregates Polaris, Trivy, OPA, kube-bench and Goldilocks into ONE
  action-item stream, so an item may be about a container image or a Kubernetes
  manifest. The component is the image and tag when there is one, otherwise the
  resource name - a single rule would mislabel half the findings. The originating
  tool becomes a tool:<report type> tag.

Also mirrored: a Fixed item imports closed rather than active; the resource line
is namespace/kind/name with an optional container qualifier, skipping missing
segments; CVEs come from the prose since Fairwinds has no CVE field; and the
cluster tag is added unconditionally, so an item with no cluster gets a bare
"cluster:" tag - reproduced rather than tidied, since tidying would be a
divergence between a file import and an API sync.

JSON keys are PascalCase (Title, Severity, ResourceKind).

* feat(parsers): add AccuKnox parser

Mirrors pkg/tools/accuknox/converter field for field, uses the connector's
ScanType verbatim, and copies its dedup settings.

The defining problem: AccuKnox returns container, IaC, cloud-posture and runtime
findings through ONE endpoint with different column names per type, and does not
publish that part of its schema. The converter therefore probes a list of
candidate keys per field, and so does this parser - assuming one set of names
would silently import empty Info findings for every type but one. Every candidate
is also tried with AccuKnox's "vulnerability__" column prefix, which some rows
use. The fixture deliberately mixes all three conventions so a regression shows
up as a failing test rather than blank findings.

Status handling: only fixed, accepted-risk and duplicate close a finding; the
working states (in progress, waiting for 3rd party, exception requested, waiting
for verification) stay OPEN so work in progress is not hidden. A finding is
verified unless the status is empty or "potential" - note a blank status counts
as verified, the opposite of a plain truthiness check. A row AccuKnox has ignored
is marked out of scope rather than dropped, and that flag arrives as a boolean or
a string, so both are handled.

CVEs come from the CVE column, which may be an array, falling back to the title
since AccuKnox often carries the identifier only there.

* style(accuknox): clear three ruff errors that were left outstanding

I committed the parser with these unfixed because I checked lint with 'tail -1',
which showed only the trailing help line and hid 'Found 3 errors'. Check the
error count, not the last line.

All three are rewrites with no behaviour change; the 24 tests still pass. The
verified rule is now spelled 'status not in {"", "potential"}', which reads
closer to the connector's intent than the chained comparison did.

* feat(parsers): add Halo Security parser

Mirrors pkg/tools/halosecurity/converter field for field, uses the connector's
ScanType verbatim, and copies its dedup settings.

Three things that matter:

- Halo splits an issue across TWO calls. The list row carries the issue, target
  and status; the description, category, CVEs and PCI flag exist ONLY on a
  per-issue detail. A row-only import would produce findings with no prose at
  all, so the parser merges a detail supplied as a top-level map keyed by issue
  id, a top-level array, or nested on the row.
- Severity is an INTEGER level with 5 highest - the inverse of a priority number.
  Reading it as a score, or assuming 1 is worst, would invert the ladder. The
  row's level wins, falling back to the detail's, because the list response
  sometimes omits it.
- Identity is "<issue id>:<target id>", because Halo reports the same issue once
  per affected host. Keying on the issue alone would collapse two hosts into one
  finding - and their statuses often differ, which the fixture covers.

This scan type's dedup config includes ENDPOINTS, so the scanned host is always
recorded; an unpopulated endpoint would leave the hash computed over nothing. A
test pairs that assertion with the dedup field list so the two cannot drift.

Also mirrored: only confirmed/fixing/fixed count as verified, since a new or
investigating issue has not been confirmed by anyone; Halo's literal "Nobody"
assignee is not reported; and the finding is dated today because Halo's list
response carries no discovery date.

* feat(beagle): add Beagle Security file parser

Adds a file parser for Beagle Security DAST reports, matching the scan type
"Beagle Security - Connectors Import" so a file import and an API sync
deduplicate against each other instead of producing two copies of every
finding.

Beagle documents the report-level keys and the occurrence block but not the
names of the per-finding fields, and their sample report omits the finding
array, so the parser reads every field from the same alias set the API path
uses and locates the finding array by name or, failing that, by shape.

- one finding per occurrence; "Fixed" (the only documented status) imports as
  mitigated, every other status as open
- severity label first, falling back to grading a bare CVSS score against the
  9.0 / 7.0 / 4.0 floors; an unrecognised label becomes Info and is kept as a tag
- accepts both the API envelope, which carries the report as a JSON string, and
  the report body itself
- records the tested URL, since this scan type's hash includes endpoints

37 tests, four sample reports, docs page, and the two settings entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(nightfall): add Nightfall AI file parser

Adds a file parser for Nightfall AI DLP violations, matching the scan type
"Nightfall AI - Connectors Import" so a file import and an API sync deduplicate
against each other instead of producing two copies of every violation.

Nightfall needs two calls per violation - the violation, then the detections
that make it up - and a detection carries no violation id, so the parser accepts
the detections keyed by violation id or nested on each violation. Without them a
finding loses its evidence, its credential verdict and its severity.

- a credential Nightfall verified as live is Critical whatever the policy risk
  says; otherwise the risk label is used and an unrecognised one becomes Info
- PENDING imports as active but unverified, EXPIRED as out of scope
- location, permalink and exposure note are per-integration mappings, mirroring
  the connector; GitHub violations are the only ones with a code location
- only redacted detection text is read, so no secret value is imported

39 tests, three sample exports, docs page, and the two settings entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(fleet): add Fleet vulnerabilities and policies file parsers

Adds two file parsers for Fleet host exports, matching the scan types
"Fleet:Vulnerabilities - Connectors Import" and "Fleet:Policies - Connectors
Import" so a file import and an API sync deduplicate against each other instead
of producing two copies of everything.

They are two parsers because Fleet's API models software CVEs and policy results
as different things: the connector imports them under two scan types with
different deduplication keys, one hashing the component and the other the policy.
Each parser reads the same host export and ignores the other's half.

Vulnerabilities:
- a finding per host per software per CVE; identity "<host>:<software>:<version>:<CVE>"
- an unscored CVE is Medium, not Info - Fleet enriches from the NVD, so no score
  means "not scored yet"; an explicit zero is Info
- CISA KEV membership is flagged and tagged but does not change the severity
- scores may arrive as numbers or numeric strings

Policies:
- only a failing policy is a finding; passing, result-less and unnamed ones are
  skipped
- High when Fleet marks the policy critical, Medium otherwise - Fleet has no
  other severity signal for policies
- the policy query is included so a reviewer can see what was checked

43 tests, six sample exports, two docs pages, and four settings entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(elastic-security): add CNVM, posture and detections file parsers

Adds three file parsers for Elastic Security exports, matching the scan types
"Elastic Security:CNVM - Connectors Import", "Elastic Security:Posture -
Connectors Import" and "Elastic Security:Detections - Connectors Import" so a
file import and an API sync deduplicate against each other instead of producing
two copies of everything.

Elastic returns all three kinds of document from the same _search API with the
same ECS asset objects, and each is imported under its own scan type with its own
deduplication key - the component for CNVM, the rule for the other two. So there
are three parsers, each claiming only its own documents: one export can be
uploaded three times without a document landing under the wrong key. The shared
document walk and asset rendering live on the CNVM module, the way the shipped
Invicti parser extends the Netsparker one.

CNVM:
- Elastic's severity label wins over the CVSS score; an unrecognised label falls
  back to grading the score, and only a v3 base reaches the cvssv3_score field
- identity is the Elasticsearch document id, which is stable across syncs

Posture:
- only a failing, named benchmark rule is a finding
- an unrecognised label is Medium, not Info - there is no score to fall back on
- a description that merely repeats the rationale is not printed twice

Detections:
- alerts are read from kibana.alert or the older top-level signal object
- imported as neither static nor dynamic, with a triage instruction rather than a
  fix: a detection is observed activity, not a defect with a patch
- Elastic's 0-100 risk score is reported, not converted into a severity

62 tests, nine sample exports, three docs pages, and six settings entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(parsers): guard every endpoint host against DefectDojo's validator

Endpoint.clean() accepts a host of letters, digits, dot, hyphen, underscore or
plus - or an IP address - and raises ValidationError otherwise. Because it
raises, one unusable host fails the WHOLE import rather than the one finding.

Codacy hit exactly that in CI: a container item's affectedTargets is an image
reference, so "registry.example.com/generic-app" went into the host field and
took the import down with it. It is now parsed, giving host + path, which is what
DefectDojo does with the endpoint string the connector sends. Two more parsers
were hand-splitting a URL in a way that left "host:port" in the host field -
Halo Security and YesWeHack - and both now parse instead.

Every parser that records a host also checks it first and drops the endpoint
rather than raising, because the values are free text: a Fleet display name is
often "Someone's MacBook", and an Elastic cloud resource name can be a path. The
value still appears in the description, so nothing is lost.

Nine parsers touched: beagle, codacy, crowdstrike_spotlight,
elastic_security_cnvm (shared by the three Elastic scan types), fleet_policies,
fleet_vulnerabilities, halosecurity, probely, yeswehack. Nine new tests pin the
behaviour, including IPv6 hosts, which the pattern rejects but the IP check
accepts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(detectify): assert an unset location path as falsey, not None

An unset path is "" on the URL location model (CharField(blank=True)) and None
on Endpoint, so asserting either one specifically passes under one value of
V3_FEATURE_LOCATIONS and fails under the other. This test asserted None and so
failed the locations leg in CI while passing the endpoint leg.

The local harness had the same coercion, which is why it did not reproduce; the
harness now keeps "" as the real model does, and reproduced this failure exactly
and no others.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(action1): add Action1 file parser

Adds a file parser for Action1 vulnerability exports, matching the scan type
"Action1 Scan" so a file import and an API sync deduplicate against each other
instead of producing two copies of everything. Note the scan type does not follow
the "<Vendor> - Connectors Import" pattern, so it is copied rather than derived,
and a test asserts the derived form is not claimed.

Action1 describes a finding across two calls - the vulnerability catalogue and the
machines each CVE affects - so the parser accepts the affected endpoints keyed by
CVE or nested on the vulnerability. A catalogue entry nothing is running produces
no finding, mirroring the connector.

- identity is "action1-<CVE>-<endpoint id>": one CVE on three machines is three
  findings, and the endpoint's own copy of the software wins because the installed
  version differs per machine
- Action1's "score" is a word, not a number; it is the fallback when base_severity
  is absent, and an unrecognised bucket is Info
- mitigation lists Action1's available updates and is left empty when it knows of
  no patch, rather than filled with generic advice
- the machine name is recorded as an endpoint only when it can be a host; Action1
  names are free text

28 tests, three sample exports, docs page, and the two settings entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(datadog): add Datadog Cloud Security file parser

Adds a file parser for Datadog Cloud Security findings, matching the scan type
"Datadog Cloud Security" so a file import and an API sync deduplicate against
each other instead of producing two copies of everything. The scan type does not
follow the "<Vendor> - Connectors Import" pattern, so it is copied rather than
derived.

Datadog returns misconfigurations, library and code vulnerabilities, attack paths,
identity risks and API-security findings through one endpoint, told apart only by
finding_type - so this parser decides static versus dynamic per row rather than
per file. Its attributes are also nested twice, and reading the outer object as
the finding yields nothing.

- three independent signals mean "already dealt with" and all three are honoured:
  a muted/resolved/auto_closed status, an explicit workflow mute, and a compliance
  evaluation that passed
- base_severity is deliberately ignored; the adjusted severity is the graded one
- dates are unix milliseconds, and CVSS comes from the base block with the
  adjusted block as fallback, both values taken together
- vulnerability ids come from the advisory and from the prose, since Datadog puts
  them in different places per finding type
- tags are deduplicated but not sorted, matching the connector

26 tests, three sample exports, docs page, and the two settings entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(escape): add Escape file parser

Adds a file parser for Escape API-security scans, matching the scan type
"Escape - Connectors Import" so a file import and an API sync deduplicate against
each other instead of producing two copies of everything.

Escape nests issues under the scan that produced them and the connector reads an
application's latest scan, so the parser accepts a scan, an application carrying
one, an applications response, or the issue list itself.

- this scan type's hash includes endpoints, so the tested URL is always recorded
  with its scheme, port, path and query
- the endpoint line carries the method, because the same URL behaves differently
  per verb; the method tag is uppercased while the description keeps Escape's
  casing, matching the connector
- CWE is read from "CWE-89" or a bare number, and an unparseable value still
  appears in the description
- mitigation is left unset when Escape has no remediation, rather than filled with
  generic advice

22 tests, three sample exports, docs page, and the two settings entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(insightappsec): add Rapid7 InsightAppSec file parser

Adds a file parser for Rapid7 InsightAppSec vulnerabilities, matching the scan
type "Rapid7 InsightAppSec - Connectors Import" so a file import and an API sync
deduplicate against each other instead of producing two copies of everything.

InsightAppSec names a vulnerability only by the id of the attack module that found
it, so the readable title, the rule identity and the description prose all come
from a separate module-metadata call; the parser accepts it as a list, a map, or
the module endpoint's own response.

- only UNREVIEWED and VERIFIED rows are imported, so a reimport closes the
  remediated, duplicate, ignored and false-positive ones
- status and severity are matched case-sensitively against Rapid7's own uppercase
  enums, and the raw label is kept as the severity justification
- this scan type hashes the unique id ALONE - no title, no severity - because the
  vulnerability id is stable and a volatile field would split a regraded finding
- evidence is flattened to inert text, matching Go's EscapeString byte for byte,
  and only the first three entries are printed
- CVSS is recorded only when the vector really is v3

28 tests, three sample exports, docs page, and the two settings entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(intruder): add Intruder file parser

Adds a file parser for Intruder issues, matching the scan type
"Intruder API Import" so a file import and an API sync deduplicate against each
other instead of producing two copies of everything. The scan type does not follow
the "<Vendor> - Connectors Import" pattern, so it is copied rather than derived.

Intruder separates an issue from its occurrences - the targets it was found on -
and the occurrence is the finding, so an issue with none produces nothing. Its own
issue object carries "occurrences" as a URL string rather than a list, so the
parser accepts that second call keyed by issue id or nested on the issue, and a
test asserts the unexpanded URL is not mistaken for data.

- this is the one connector scan type using the PLAIN hash_code algorithm, with
  the occurrence id inside the hash fields rather than paired with them
- snoozing is how Intruder records triage: FALSE_POSITIVE and the two
  risk-acceptance reasons map to flags, and an unrecognised reason leaves the
  finding inactive with neither rather than guessing
- the occurrence's CVSS score wins over the issue's, since the same weakness
  scores differently per target
- a port of "0" and a target that cannot be a host are both left unrecorded

27 tests, three sample exports, docs page, and the two settings entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(nowsecure): add NowSecure file parser

Adds a file parser for NowSecure mobile-app assessments, matching the scan type
"NowSecure" so a file import and an API sync deduplicate against each other
instead of producing two copies of everything. The scan type is the bare vendor
name rather than the "<Vendor> - Connectors Import" pattern, so it is copied.

NowSecure reports every check it ran, so only rows it marks as affecting the app
and not hidden are imported. One assessment runs both a static and a dynamic
analysis of the same app, so static-versus-dynamic is decided per finding, and an
unrecognised analysis type leaves both flags at their default rather than guessing.

- identity is "nowsecure-<check id>[-<vulnerability id>]", falling back to a slug
  of the title when there is no check id
- vulnerability identifiers are SORTED and deduplicated case-insensitively, which
  is this connector's extractor behaviour and differs from the order-preserving
  path the others use
- the assessment supplies the date and platform, which the finding rows lack
- the CVSS score is set unconditionally, so an unscored finding lands as 0.0

22 tests, three sample exports, docs page, and the two settings entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(vanta): add Vanta file parser

Adds a file parser for Vanta compliance exports, matching the scan type
"Vanta Compliance" so a file import and an API sync deduplicate against each other
instead of producing two copies of everything. The scan type does not follow the
"<Vendor> - Connectors Import" pattern, so it is copied rather than derived.

A Vanta finding is a (test, failing entity) pair: the test is the control and the
entity is the resource failing it, so the parser accepts the failing entities keyed
by test id or nested on the test, in either the paged results.data wrapper or a
bare list. A test with no failing entity is the control working and produces
nothing.

- severity is always Medium: Vanta has no scale, and Info would read as
  non-actionable when a failing control is actionable by definition
- that makes component_name load-bearing - it is the failing entity, and the hash
  needs it to keep two resources failing one control apart
- only FAILING entities are imported; an entity with no status is taken at its word,
  since the connector only ever receives failing ones
- the date is when the entity started failing, falling back to the test's flip date

21 tests, three sample exports, docs page, and the two settings entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(wallarm): add Wallarm file parser

Adds a file parser for Wallarm API-security vulnerabilities, matching the scan type
"Wallarm API Security" so a file import and an API sync deduplicate against each
other instead of producing two copies of everything. The scan type does not follow
the "<Vendor> - Connectors Import" pattern, so it is copied rather than derived.

- the threat level arrives in ONE field as either a number or a word, depending on
  which API answered, so both ladders are needed; 5 is the most severe, which is
  the one part of the mapping the vendor does not document and is worth confirming
  live
- closed and false-positive rows are skipped
- identity is "wallarm-<id>", falling back to the wid and then the location, which
  is last because it is the only fallback that is not an id
- only an absolute path is appended to the endpoint: Wallarm reuses the path field
  for a parameter location on some vulnerability types
- mitigation is Wallarm's exploit example rather than advice - the only
  remediation-shaped field it has - and identifiers are sorted and deduplicated
  case-insensitively, matching the connector's extractor

25 tests, three sample exports, docs page, and the two settings entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(nowsecure): an unset analysis type lands as dynamic, not neutral

DefectDojo defaults static_finding to False and dynamic_finding to TRUE, so a
finding that leaves both alone is recorded as DYNAMIC rather than as neither. The
NowSecure parser leaves them alone for an analysis type the connector does not
recognise - which is correct, because the connector's own findings behave the same
way when they reach DefectDojo - but the test asserted two Falses and the docs
page called it "neither flag set". Both now state what actually happens.

Forcing two Falses instead would have been the wrong fix: it would make a file
import and an API sync disagree about the same finding.

The local harness stubbed every boolean as None, which is why this passed locally
and failed on all four CI legs. It now mirrors the real defaults - active and
dynamic_finding True, the rest False - and reproduced this failure exactly while
surfacing no others across all 33 parsers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(bright): add Bright Security file parser

Adds a file parser for Bright Security DAST scans, matching the scan type
"Bright - Connectors Import" so a file import and an API sync deduplicate against
each other instead of producing two copies of everything.

- this scan type's hash includes endpoints, so one is always recorded: the entry
  point Bright attacked, falling back to EVERY affected resource, because Bright
  reports one issue against several when the weakness is reachable from more than
  one URL
- the request and response go in fenced code blocks - raw HTTP captured from the
  target, which must not be read as markup and which a reviewer needs verbatim
- CWE reads "CWE-79" or a bare number, and an unparseable value still shows in the
  description; the CVSS score may arrive as a number or a numeric string
- an entry point that cannot be a host is left out of the endpoints rather than
  failing the whole import

24 tests, three sample exports, docs page, and the two settings entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(defender-for-cloud): add Microsoft Defender for Cloud file parser

Adds a file parser for Defender for Cloud sub-assessments, matching the scan type
"Microsoft Defender for Cloud - Connectors Import" so a file import and an API sync
deduplicate against each other instead of producing two copies of everything. This
is a different product from Defender for Endpoint, which DefectDojo already parses
as ms_defender.

Defender returns every sort of sub-assessment through one endpoint, and the same
field means different things in each: a container finding puts the vulnerable
package under softwareDetails while a server finding flattens it into softwareName.
Reading only one shape would leave every finding of the other kind with no
component, which is the field a reviewer patches.

- only Unhealthy sub-assessments are imported, so a reimport closes resolved ones;
  SQL baselines and posture checks are excluded by resource type, and an
  UNFAMILIAR type is decided by whether it carries a CVE - so a new Defender
  scanner is not dropped silently
- the highest CVSS base score wins, and only a v3 base reaches the v3 field; the
  justification records which version it was
- CVE ids are matched anchored, so "supersedes CVE-2000-0009" in a reference title
  does not contribute another finding's identifier
- the TVM cve field is accepted as a list, an object or a bare string
- the title appends the package only when the display name is a bare CVE
- deduplication is the ARM id alone: it already encodes subscription, resource and
  finding

25 tests, three sample exports, docs page, and the two settings entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(akto): add Akto file parser

Adds a file parser for Akto API-security issues, matching the scan type "Akto Scan"
so a file import and an API sync deduplicate against each other instead of
producing two copies of everything. The scan type does not follow the
"<Vendor> - Connectors Import" pattern, so it is copied rather than derived.

Akto runs every test against every endpoint it knows, so neither alone identifies a
finding: the identity is "akto-<collection>-<method>-<url>-<test sub-category>" and
both the endpoint and the test are in this scan type's hash fields. Akto has no
package, so component_name is "<METHOD> <url>" - the tested endpoint is what the
component slot means here.

- IGNORED is how a reviewer marks a false positive; FIXED is inactive but NOT
  flagged, because "fixed" is not a judgement about whether it was real
- a relative apiUrl is not recorded as an endpoint - the connector does not invent a
  host - but it is still the component and in the description
- CVE ids come from Akto's free-text field, sorted and deduplicated
  case-insensitively

22 tests, three sample exports, docs page, and the two settings entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(holm-security): add Holm Security file parser

Adds a file parser for Holm Security vulnerabilities, matching the scan type
"Holm Security Scan" so a file import and an API sync deduplicate against each
other instead of producing two copies of everything. The scan type does not follow
the "<Vendor> - Connectors Import" pattern, so it is copied rather than derived.

Holm scans two ways and only the web class exercises a running application, but the
class is a property of the scan rather than the row - so an export states it as a
top-level "class", and an absent one means static, matching the connector's default.

- the severity NAME wins and the numeric level is only the fallback, so an
  unfamiliar name does not become Info while a usable level sits beside it; 4 is
  the most severe, the inverse of a priority number
- the identity carries the asset and port because Holm reports one weakness per host
  and per listening port; a port of zero is left out rather than recorded as zero
- the endpoint is Holm's URL alone - the separately-reported detected_port is in the
  identity and description instead, so no endpoint is invented
- the CVSS base wins over the score, and the date prefers the last detection

25 tests, three sample exports, docs page, and the two settings entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(klocwork): add Klocwork file parser

Adds a file parser for Klocwork issues, matching the scan type "Klocwork Scan" so a
file import and an API sync deduplicate against each other instead of producing two
copies of everything. The scan type does not follow the "<Vendor> - Connectors
Import" pattern, so it is copied rather than derived.

Klocwork's search endpoint answers with NDJSON - one issue per line, not an array -
so that is the shape read first; a JSON array, an object with an issues list, and a
single issue object are accepted for an export somebody reshaped. The trailing
summary line is skipped by testing for its key rather than parsing it, and a
search that matched nothing answers with that line ALONE, which is an empty result
rather than a malformed file.

- the severity CODE is the inverse of a score: 1 is Critical, and codes 5-10 are
  Klocwork's informational tiers
- numbers may arrive quoted; both forms are read, because the connector's decoder
  silently skips a line it cannot parse and would report a clean empty sync
- Ignore, Not a problem and Filter are false positives; the deferred states the
  connector's query selects stay ACTIVE, since a deferred finding is still a finding
- file_path and the checker are both in the hash, so one checker in two files is two
  findings
- dates are unix milliseconds

24 tests, four sample exports (three NDJSON, one reshaped array), docs page, and the
two settings entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(qwiet): add Qwiet AI file parser

Adds a file parser for Qwiet AI findings, matching the scan type "Qwiet Scan" so a
file import and an API sync deduplicate against each other instead of producing two
copies of everything. The scan type does not follow the "<Vendor> - Connectors
Import" pattern, so it is copied rather than derived.

Qwiet carries the interesting metadata as a LIST of key/value tag objects rather
than as fields - the CVE, package URL, CVSS score, CWE category and reachability
verdict all live there - so each is read by key. Looking for fields of those names
would find nothing at all.

- reachability is recorded as the severity justification rather than changing the
  grade, so a reviewer can see why two findings of equal severity differ in urgency
- a dependency finding with related_findings is reachable even with no reachability
  tag: those related findings ARE the path Qwiet traced through the application
- the package URL is reduced to its last segment, because the namespace before it is
  the group rather than the artefact a component matches on
- only the first file location becomes file_path and line, since a data-flow finding
  spans several files and DefectDojo has one path; the rest stay in the description,
  and an unparseable line keeps the path
- the hash spans file_path, cwe AND component_name because Qwiet reports both code
  and dependency findings

23 tests, three sample exports, docs page, and the two settings entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(holm_security): stop asserting a port the locations model infers

The locations model fills an unset port in from the protocol (URL.clean_port
reads DEFAULT_PORTS), so an https URL naming no port is port 443 there while
Endpoint leaves it None. The test asserted the port was falsy against a
fixture whose detected_port is 443 on an https URL - a number both sources
produce - so it passed with V3_FEATURE_LOCATIONS off and failed with it on.

Assert against the fixture row whose detected_port is 8443 on a URL naming no
port instead: a value neither the URL nor the scheme default can produce, so
it distinguishes the two sources in both modes. The scheme-default case keeps
its own test, covering the description and identity where the port does belong.

The parser is unchanged - the connector puts only the URL in the endpoint.

* feat(automox,bigid): add Automox and BigID file parsers

Both mirror their connector's finding converter field for field, and both
report the connector's own scan type verbatim so a file import and an API sync
deduplicate against each other rather than producing two copies of everything.

Automox reports a missing patch and the device it is missing on from two
different endpoints, so an export carries both lists and they are joined on
the package's server_id. A package whose device is absent is still a finding:
the connector's device lookup is a map read that can miss, and it converts
anyway. A row with no usable id is dropped, because the id is the whole
identity and every row without one would collapse onto "automox-0".

BigID reads sensitive data at rest, so only the case identity, its policy and
data-source context and the COUNT of affected objects are carried. No sample,
preview or value of the data itself is read, even when the export contains
one - covered by a fixture that includes those fields and a test that asserts
none of them reach the finding.

* feat(calicocloud,dragos): add Calico Cloud and Dragos file parsers

Both mirror their connector's finding converter field for field and report the
connector's own scan type verbatim, so a file import and an API sync
deduplicate against each other rather than producing two copies of everything.

Calico Cloud serves the image list and each image's vulnerabilities from two
endpoints, so an export carries both - nested in the image or keyed by image
id. An image whose scan result is still "Unknown" is skipped entirely, as the
connector does: its results are unfinished, and importing them would record a
partial scan as a complete one. Severity comes from CVSS rather than Calico's
Pass/Warn/Fail verdict, whose thresholds are per-tenant configuration.

Dragos grades on its own 0-5 scale where 5 is the most severe - the inverse of
a score - so CVSS wins where both exist and the scale is read as a scale, not
as a score. Its OT exploitability intel is recorded as the severity
justification rather than moving the severity. pera_level is absent rather
than zero when unknown, and Purdue level 0 is a real level (the physical
process layer), so the two do not render alike.

* feat(finitestate): add Finite State file parser

Mirrors the connector's finding converter field for field and reports its scan
type verbatim, so a file import and an API sync deduplicate against each other.

The VEX status carries the real semantics. NOT_AFFECTED is a product team
asserting the vulnerability does not apply to this build, so it lands inactive
and out of scope rather than active - leaving it active would put an answered
question back in the queue on every import. It is additionally a false
positive when the justification says the vulnerable code is not there to be
reached; a justification like inline mitigations means the flaw is real but
handled, which is out of scope and NOT a false positive. Anything unrecognised
stays active, the safe direction to be wrong in.

"unknown" is a value the platform actually uses and means Info, so it does not
fall through to the CVSS-derived severity - treating it as missing would
silently upgrade every unscored finding. EPSS is per-CVE, so the highest is
taken with its own percentile rather than mixing one CVE's score with
another's percentile.

This scan type has no hashcode field list in the connector settings, so none
is registered here: it deduplicates with DefectDojo's default algorithm, which
is what the connector's own findings already do.

* feat(fortytwocrunch): add 42Crunch file parser

Mirrors the connector's converter field for field and reports its scan type
verbatim, so a file import and an API sync deduplicate against each other.

42Crunch produces two reports for the same API and the connector converts both
under one scan type: a Security Audit of the OpenAPI definition (static, one
finding per issue occurrence) and a Conformance Scan of the running API
(dynamic, one per scan issue). A file is one or the other, so the shape
decides.

Neither report stores its text inline - an audit occurrence's pointer and a
scan issue's description and location are integer indexes into the report's own
lookup tables, so each is resolved. An out-of-range index resolves to nothing
rather than failing the import, and an audit identity then keeps the raw index,
without which two occurrences of one issue at no resolvable location would
collapse into a single finding. Scan descriptions are templates filled from a
separate parameter list, one substitution per parameter.

A scan issue's own id is a per-scan UUID, so the identity uses the operation
plus the check index - stable for the same issue across scans. The API id
prefix that every connector identity carries is not in a downloaded report, so
a wrapper may supply it; the docs explain that findings will not deduplicate
against synced ones without it.

* test: compare default dates against the UTC date, not the local one

Finding.date defaults to get_current_date(), which is timezone.now().date() -
the UTC date, since USE_TZ is on and TIME_ZONE is "UTC". Four of the new tests
compared it against date.today(), the LOCAL date, which agrees only while both
happen to fall on the same day. The other fourteen parser tests on this branch
already use the UTC form; this makes these four match.

* feat(hiddenlayer): add HiddenLayer model-scan file parser

Mirrors the connector's finding converter field for field and reports its scan
type verbatim, so a file import and an API sync deduplicate against each other.

HiddenLayer reports SARIF and DefectDojo already parses SARIF - but importing
through the generic parser records the findings under the "SARIF" scan type,
where they would not deduplicate against the connector's. The mapping here is
the connector's, which itself mirrors dojo/tools/sarif/parser.py.

Three SARIF behaviours carry…
…t only narrow them (DefectDojo#15502)

The candidate hook could already only be used to drop candidates the deduplication
algorithm had resolved. A plugin that identifies a finding by something this module does
not know about had no way to contribute a match, even though the call site already
replaces the candidate list wholesale rather than intersecting with it. This states that
as the contract and covers it with a test, so a later refactor cannot quietly turn the
hook back into a filter.

Adding candidates carries obligations this function cannot check without giving up its
single-query fetch: stay inside the product, or a false-positive verdict replicates across
a boundary the user never crossed; exclude the findings being processed, or a finding can
mark itself; and have the read fields loaded, since candidates are fetched with .only().
The first two need the batch's scope, which the hook never saw -- it is handed one finding
at a time -- so FalsePositiveCandidateContext now carries the product, the algorithm and
the excluded ids.

The context is passed only to hooks that accept it, so a hook written against the
two-argument form keeps working; the two existing hook tests cover that form.

Co-authored-by: devGregA <greg-agent-2@defectdojo.com>
)

Port of DefectDojo#15501, which landed on bugfix only. dev is where this has actually
bitten: two changes each adding a migration on the same parent go green
individually, because the fork exists only in their union, and nothing
re-derives the graph after a merge.

Until a release merge-back carries DefectDojo#15501 across, the branch that most needs
the check is the one without it. Cherry-pick rather than reimplement, so the
two lines stay byte-identical and the next merge-back is a no-op.

Verified on this branch: the dojo graph is sound (199 migrations, 90 squashed
out, single leaf 0288_backfill_vulnerability_id_entities) and all 22 tests pass.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…rs can reuse it (DefectDojo#15489)

* refactor(finding): extract save()'s field derivation so batched writers can reuse it

Finding.save() derives the finding's own columns -- title casing/truncation, blank
component normalization, the date default, numerical_severity, CVSS v3/v4 parsing, and
the same-tool hash -- inline, mixed in with work that needs a primary key.

Only the former is meaningful to a caller writing rows in bulk. bulk_create and
bulk_update bypass save() and its signals entirely, so a batched writer must either
reimplement that derivation or write rows that differ from every other finding: wrong
casing, no numerical_severity, an unparsed CVSS vector, no hash.

Reimplementing it has already cost us once. A downstream hash re-derived only the title
truncation and omitted titlecase(); because titlecase() also collapses whitespace, the
pre-save lookup hash and the stored hash diverged for any multi-line title, so reports
with an embedded newline pair matched nothing on reimport and were closed and recreated
on every run.

Adds Finding.persisted_title() as the single source of truth for the title transform
(reading max_length rather than hardcoding 511) and Finding.derive_persisted_fields(),
which is the existing block moved verbatim and called by save() at the same point.
Fields needing a PK stay in save(): a batched writer needs a set-based implementation of
those, not a shared one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(finding): fold the new-finding static/dynamic derivation into the shared method

The flags a new finding gets from file_path plus its parser-attached locations are
derived in memory -- no row required -- so they belong with the rest of the derivation a
batched writer needs, not in save()'s body where a bulk path would have to duplicate
them. The equivalent branch for an existing finding queries self.locations/endpoints and
stays in save().

save() passes is_new_finding through, so its behavior is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* style(finding): put the new docstring summaries on the second line

Upstream ruff enforces D213 (multi-line-summary-second-line); both new docstrings used
the summary-on-first-line form.

Caught by CI rather than locally: this repo's ruff.toml pins a rule selector my local
ruff rejects outright, so the config cannot be loaded here and the whole file lints as
unrunnable rather than clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Greg Anderson <greg@Gregs-MacBook-Pro.local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
dev still runs the CI this repository had a week ago: the 113-job per-file
Selenium matrix, serial unit tests that migrate from scratch, no snapshot
restore, no grouping, no tiers, no merge_group triggers, no gate context.
Every pull request against dev pays roughly 565 runner-minutes and
queue-bound wall clock that bugfix stopped paying days ago -- and the gap
would persist until a release merge-back happens to carry the workflow
files across.

This ports the CI stack directly: fifteen files, each checked out verbatim
from bugfix and verified blob-identical to it, so the next release
merge-back reduces to a no-op for all of them. It is the union of what
landed on bugfix as DefectDojo#15505 (parallel unit phase + in-process celery
broker + tblib), DefectDojo#15507 (migrated-database snapshot restore + warm
workflow), DefectDojo#15521 (merge_group triggers + the Unit Tests Complete gate),
DefectDojo#15522 (Docker Hub pull retry), DefectDojo#15524 (grouped Selenium matrix +
sequential-group entrypoint + the async-test product cleanup and paged
finding purge), DefectDojo#15528 (two-tier matrices), DefectDojo#15529 (push-trigger scoping +
the makemigrations heredoc that misdiagnosed forked graphs), and DefectDojo#15545
(the merge-queue documentation comment).

Divergence check before copying, per file, against the merge-base: dev had
not modified any of the fifteen since the branches diverged, except
migration-graph.yml, whose only difference is that dev's copy (from
 DefectDojo#15504) predates the push-trigger scoping -- bugfix's copy is that file
plus the scoping, so the verbatim copy is the correct merge there too.

What changes for dev pull requests: the light tier (23 jobs, ~130
runner-minutes) replaces the old full fan-out on every push, and the
"Unit Tests Complete" context starts reporting. What does not change: the
merge queue is NOT enabled on dev -- ruleset 20466211 targets bugfix only,
so merge_group stays inert here exactly as it was on bugfix before the
ruleset, and merging on dev works as before. Extending the queue to dev is
a one-line ruleset change to make deliberately, after it has soaked on
bugfix.

After this merges, dispatch "CI: Warm Caches" on the dev ref once so the
snapshot exists in dev's cache scope; until then the first pull requests
pay one cold migrate-and-save, which is the designed fallback.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ectDojo#15548)

* refactor(reimporter): hold findings, not ids, until dispatch

batch_finding_ids accumulated (finding.id, push_to_jira) per finding, but
those ids are only consumed in the flush block below. Holding the finding
and reading .id at dispatch defers the only primary-key read in the
per-finding loop body to a point where every finding is certainly written.

That makes the loop primary-key free, so an importer that buffers inserts
and flushes them per batch can do so by overriding
process_finding_that_was_not_matched alone, rather than copying the whole
of _process_findings_internal -- the same accommodation get_original_findings
and get_reimport_match_candidates_for_batch already provide, and which
their docstrings describe for exactly this case.

No behavior change: the same (id, push_to_jira) pairs reach the same
dispatch, in the same order.

* docs(reimporter): claim only what the deferral achieves

Self-review correction. The prior comment said a buffering importer could
get by 'by overriding process_finding_that_was_not_matched alone'. That
overstates it: the batch-boundary block persists locations and applies
parser/inherited tags -- all of which need written rows -- before the ids
are read, and none of that is behind an importer-controllable seam.

Deferring the id read removes the only DIRECT primary-key read in the
per-finding loop body. That is what this change does; the comment now says
exactly that.

* fix(reimporter): always dispatch the final post-processing batch

The batch-boundary flush lived inside the per-finding loop, guarded by
'len(batch) >= size or is_final'. The matched branch's force_continue --
taken whenever an incoming finding matches an existing false positive /
out of scope / risk accepted finding whose statuses agree -- skips the
rest of the loop body, including that check. Findings are content-sorted
before processing, so any report whose LAST sorted finding took that path
never flushed its tail: for reports under IMPORT_REIMPORT_DEDUPE_BATCH_SIZE
(1000), the entire report silently skipped post_process_findings_batch
(deduplication, rules, issue updater, JIRA) and parser/inherited tag
application. Locations and vulnerability ids were only rescued later if
close_old_findings ran, which it does not when close-old is off.

The same loss applied when the final iteration's finding was falsy (the
'appears to always be true' guard).

Fix: extract the flush block into _flush_post_processing_batch and call it
from two places -- size-triggered inside the loop, and once unconditionally
after the loop as a drain. The drain replaces the is_final machinery
(is_final_batch / is_final / the enumerate index existed only to feed that
condition), so the final flush no longer depends on how the last iteration
ended. Every step is a no-op on empty state, which close_old_findings
already relies on by calling persist() unconditionally.

The regression test drives the real matching path: an existing false
positive, an incoming status-parity match crafted to hash-match it and
sort last, and one genuinely new finding. Without the drain, the dispatch
mock records no batch at all.

* test(reimporter): drop the unused unpacked variable

ruff (CI pins 0.16.0) flags the unused 'to_mitigate' from the empty-report
drain test's tuple unpack. Renamed to _to_mitigate.

Caught late because I had been running ruff --isolated: this repo's
ruff.toml selects PLW0717, which only exists from 0.16.0, so a local 0.15.x
cannot parse the config at all. Falling back to --isolated silently drops
the repo's whole rule set. 'uvx ruff@0.16.0 check .' is the parity command.

* perf(locations): skip the empty transaction when nothing is buffered

LocationManager.persist() opened transaction.atomic() unconditionally.
_persist_locations() and _persist_status_updates() both already
short-circuit on empty accumulators, so with nothing buffered the only
cost of proceeding was an empty transaction -- and inside an outer atomic
block that is a SAVEPOINT/RELEASE pair, two queries to do nothing.

persist() is called at every batch boundary and again unconditionally by
close_old_findings, so imports that touched no location paid those pairs
already. The post-loop drain added in this branch made it measurable: the
performance suite moved expected_num_queries4 by exactly +2.

The guard is the union of the two inner guards, so it can only skip work
that both inner methods would have skipped anyway.

* test: drop the V3 reimport baselines by the removed empty transaction

LocationManager.persist() no longer opens transaction.atomic() when
nothing is buffered, so the SAVEPOINT/RELEASE pair that used to cost two
queries on the persist() call with nothing to write is gone. A reimport
makes two persist() calls -- the batch boundary and close_old_findings --
and one of them has nothing to write.

94 -> 92 (no change) and 195 -> 193 (with new findings). V2 is unaffected
because EndpointManager.persist() opens no transaction, which is why only
the V3 constants move.

---------

Co-authored-by: Greg Anderson <greg@Gregs-MacBook-Pro.local>
Bumps [pillow](https://github.com/python-pillow/Pillow) from 12.2.0 to 12.3.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](python-pillow/Pillow@12.2.0...12.3.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-version: 12.3.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…/workflows/validate_docs_build.yml) (DefectDojo#15526)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…v44 (.github/workflows/renovate.yaml) (DefectDojo#15473)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…fectdojo/chart.yaml) (DefectDojo#15532)

* chore(deps): update valkey docker tag from 0.24.6 to v0.25.0 (helm/defectdojo/chart.yaml)

* update Helm documentation

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Bumps [redis](https://github.com/redis/redis-py) from 8.0.1 to 8.1.0.
- [Release notes](https://github.com/redis/redis-py/releases)
- [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES)
- [Commits](redis/redis-py@v8.0.1...v8.1.0)

---
updated-dependencies:
- dependency-name: redis
  dependency-version: 8.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…o#15538)

Bumps [django-polymorphic](https://github.com/django-commons/django-polymorphic) from 4.11.6 to 4.11.7.
- [Release notes](https://github.com/django-commons/django-polymorphic/releases)
- [Commits](django-commons/django-polymorphic@v4.11.6...v4.11.7)

---
updated-dependencies:
- dependency-name: django-polymorphic
  dependency-version: 4.11.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…1.6.0 (requirements-dev.txt) (DefectDojo#15544)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…ile.nginx-alpine) (DefectDojo#15546)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…lassified and versioned (DefectDojo#15556)

Replaces the create-only lifecycle section. Editing was blocked because a mapping
edit can change what a finding's identity is built from -- true of some edits and
not others -- so the page now documents the distinction the API makes, the
acknowledgement an identity-relevant edit requires, the impact endpoint for
checking before committing, the transition window and its two limits, and the
revision history. Rolling forward to a new parser stays documented, as the right
answer when a report format changes enough to yield a different set of findings.

Also flags the Django admin as the one path that bypasses versioning.

Co-authored-by: DefectDojo Agent <greg-agent-4@defectdojo.com>
Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.10.2 to 3.10.3.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md)
- [Commits](Python-Markdown/markdown@3.10.2...3.10.3)

---
updated-dependencies:
- dependency-name: markdown
  dependency-version: 3.10.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.0 to 0.16.1.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.16.0...0.16.1)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.16.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [pyopenssl](https://github.com/pyca/pyopenssl) from 26.3.0 to 26.4.0.
- [Changelog](https://github.com/pyca/pyopenssl/blob/main/CHANGELOG.rst)
- [Commits](pyca/pyopenssl@26.3.0...26.4.0)

---
updated-dependencies:
- dependency-name: pyopenssl
  dependency-version: 26.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…efectDojo#15542)

Bumps [drf-spectacular-sidecar](https://github.com/tfranzel/drf-spectacular-sidecar) from 2026.7.1 to 2026.8.1.
- [Commits](tfranzel/drf-spectacular-sidecar@2026.7.1...2026.8.1)

---
updated-dependencies:
- dependency-name: drf-spectacular-sidecar
  dependency-version: 2026.8.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…/workflows/validate_docs_build.yml) (DefectDojo#15527)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Bumps [django-crispy-forms](https://github.com/django-crispy-forms/django-crispy-forms) from 2.6 to 2.7.
- [Release notes](https://github.com/django-crispy-forms/django-crispy-forms/releases)
- [Changelog](https://github.com/django-crispy-forms/django-crispy-forms/blob/main/CHANGELOG.md)
- [Commits](django-crispy-forms/django-crispy-forms@2.6...2.7)

---
updated-dependencies:
- dependency-name: django-crispy-forms
  dependency-version: '2.7'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
… UI (DefectDojo#15567)

* docs(notifications): note where notification settings live in the Pro UI

The Pro UI now serves notification settings as three pages -- Personal, System and
Template -- instead of the classic single page with a Scope drop-down, and the webhook
page moved with them.

Added as a Pro callout on the two configuration pages rather than a rewrite: the
Scope drop-down is still exactly how it works in open source, so the existing
instructions stay correct for that audience.

Pairs with the DefectDojo Pro change on the same release line.

* docs(notifications): use the site's Pro-note style, not an undefined shortcode

The Pro callout used `{{% alert %}}`, a Docsy shortcode this site does not define, so
`hugo` failed the docs build outright:

    failed to extract shortcode: template for shortcode "alert" not found

Those two were the only `{{% alert %}}` uses in the whole content tree, which was the
tell. Replaced with the highlighted-span convention the docs already use for Pro-only
notes. Verified with a local `hugo` build rather than another CI round-trip.
…tDojo#15472)

An engagement whose `status` or `engagement_type` column holds NULL (or an
empty string) could not be saved at all. Both fields are declared `null=True`
but neither was given `blank=True`, and Django counts `None` among a field's
empty values, so `full_clean()` -- which Engagement runs on every save --
rejected the row with `{'status': ['This field cannot be blank.']}`: the model
refusing a value its own column permits.

Every import and reimport writes its engagement back at the end of the run
(`save_without_resurrecting(self.test.engagement)`), so one such row turned
every subsequent scan ingest into that engagement into a hard failure. Through
the API the Django ValidationError is translated to HTTP 400, so callers got a
rejected import rather than any findings.

Neither column offers an empty choice and both declare a default, so an empty
value carries no meaning the rest of the codebase can read -- filters, reports
and the UI all assume one of the listed choices. `Engagement.pre_save_logic`
now fills an empty value in from the field's own default, which keeps a value
outside the choice list from becoming valid (as widening to `blank=True` would)
and lets each affected row heal the next time anything saves it. No data
migration: `pre_save_logic` runs before `full_clean`, so the very save that
used to fail is the one that repairs the row, and affected engagements recover
on their next (re)import rather than needing a backfill on deploy.

Tests cover both fields on the model save path, the import and reimport
write-backs, and that a populated value is never overwritten.


Claude-Session: https://claude.ai/code/session_01HzJ7XLk2RsabJbYyJJgxvg

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Bumps [cryptography](https://github.com/pyca/cryptography) from 49.0.0 to 50.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](pyca/cryptography@49.0.0...50.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 50.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…lk (DefectDojo#15571)

Documents the DISA STIG Checklist scan type: the two STIG Viewer formats it
reads, how the four checklist statuses become finding states, how DISA
categories and assessor severity overrides map to severity, what each finding
carries, and how findings are identified.

Two points get more than a passing mention because they change how someone
organizes their imports. Findings are identified by V-number *on the assessed
asset*, so the same rule failing on two hosts stays two findings and a STIG
release upgrade keeps history. And closing on re-import is driven by an item's
absence from the report, so a test holding several assets' checklists needs
Close Old Findings turned off.

On the compliance side, STIG rules cite CCIs rather than naming controls, so
control coverage gains a section on the CCI crosswalk, the precedence between
mapping sources, and the fact that the backfill command now runs both passes.
The compliance profile's configuration-test-types note gains the STIG case,
including why it is not switched on for you.

Co-authored-by: devGregA <greg-agent-2@defectdojo.com>
* refactor(ui): remove the classic Bootstrap UI

The deprecation banner announced that the classic UI is retired and the
redesigned UI becomes the default in 3.3.0. This removes the classic tree
and the machinery that chose between the two.

Removed:
- dojo/templates_classic/ (the Bootstrap 3 / SB Admin 2 tree)
- dojo/template_loaders.py: UIPreferenceLoader picked a tree per request
- UserContactInfo.ui_use_tailwind and the opt-in banner it gated
- dojo/static/dojo/{css,js}/classic/ and the vendored Bootswatch build
- 16 npm dependencies that only the classic UI used

TEMPLATES is now a plain filesystem + app-dirs chain over dojo/templates,
wrapped in the cached loader outside debug mode. The per-tree caching that
UIPreferenceLoader did internally is preserved that way.

Three templates were shadowed rather than superseded: UIPreferenceLoader
searched both trees ahead of the filesystem loader, so the classic copy won
for every user and the consolidated app-dir copies underneath had gone
stale. Deleting the tree naively would have regressed them, so the live
versions are ported across:

- notifications/mail/scan_added.tpl and webhooks/scan_added.tpl kept the
  duplicate-findings sections added in DefectDojo#15007
- dojo/action_history.html kept has_active_filters from DefectDojo#15082

Two exceptions keep the app-dir copy deliberately: alert/other.tpl, whose
|safe removal (344c913) the classic copy never received, so escaping in
alert notifications is restored; and the GitHub form templates, which the
Tailwind rebuild edited on purpose.

DataTables no longer loads the Bootstrap styling integrations; markup is
styled against DataTables' own dt-* classes, which datatables-dd.css was
already written for. Two visual regressions this surfaced are fixed here:
the control row is inlined again (the integration supplied form-inline),
and report_base.html states its heading scale and opts out of the app's
uppercase h6 label styling, both of which the vendored Bootstrap build
used to provide.

Verified against a running instance, before and after, plus 163 unit tests
covering report rendering, search, notifications, and audit log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CEcRPkdyRP6wtEnqVkSB9a

* chore(deps): drop three unused npm packages and fix report icons

Follow-up audit of the Dockerfiles and package.json.

The Dockerfiles referenced none of the removed paths, so they needed no
change. collectstatic copies all of components/node_modules into the nginx
image, so an unused dependency is shipped, not just installed:

- alpinejs and htmx.org: the app loads committed vendored copies from
  dojo/static/dojo/js/vendor/, and nothing references the npm paths
- font-awesome: only report_base.html loaded it, and it was the v4 build

Dropping font-awesome means reports use the v6 build that the rest of the
app already uses, which ships with the fontawesomefree pip package rather
than npm. That also explains a comment repeated across seven report
templates -- "for some reason the font-awesome icons don't work with the
report template". Two causes, both fixed here: the templates use v6 class
names (fa-solid) that the v4 build does not define, and report_base.html
sets font-family on `*` with !important, which overrode the icon font and
rendered every icon as a missing glyph.

Verified in the browser: icons resolve to Font Awesome 6 Free and render
at full width instead of a fallback box, with body font and heading scale
unchanged. Dependencies are down to 19, from 37 before this branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CEcRPkdyRP6wtEnqVkSB9a

* fix(ui): restore the handles the UI suite navigates by, and the jQuery dropdown API

With the classic tree gone the admin user renders the redesigned templates, and
the Selenium suite lost three things it had been navigating by. None of the
failures were assertion failures -- every one was a NoSuchElementException.

The sidebar repeats every product tab label. "Engagements", "Findings",
"Endpoints", "Components" and "Metrics" all appear in the sidebar, which renders
~60 anchors before the product tab bar, so By.LINK_TEXT / By.PARTIAL_LINK_TEXT
resolved to the sidebar entry. Those entries are bound to Alpine with
@click.prevent, so the click never navigated -- it expanded a submenu, left the
tab's dropdown closed, and the failure surfaced one step later on a menu item
that was in the DOM but not rendered, so link text could not match it. The tab
bar now carries data-testid handles and tests go through a new
BaseTestCase.open_product_tab() helper that documents the trap.

The classic tree carried ids the redesigned tree never picked up:
simple_search_submit on the search button, product_component_view and
id_user_menu and menu_configuration in the sidebar. They are restored on the
equivalent elements, under the same permission gates. The Configuration section
expands on click rather than hover, so the one test that hovered it now clicks.

$.fn.dropdown was missing. index.js replaces bootstrap.min.js by delegating
clicks, but never registered the jQuery plugin, so the inline onclick handlers in
view_test, findings_list_snippet, finding_related_actions and view_objects died
with "$(...).dropdown is not a function". It is now backed by the same open/close
logic, resolving the toggle, the .dropdown container and the .dropdown-menu alike.
This one was user-visible, not just a test problem.

The eight reported failures were only the first failure in each of the nine CI
groups: the runner uses failfast=True and the entrypoint stops a group at its
first failing file, so most of the suite never ran. Fixing only those eight would
have surfaced the next one a CI cycle later, so the same breakage is fixed at all
~30 affected call sites, and all 41 files in the UI matrix were run locally.

Also adds data-testid="report-link" to the engagement, test and organization
report menu items, which By.PARTIAL_LINK_TEXT "Report" could no longer reach past
the sidebar's "Reports" section.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rpv46XWjWXCWTQSkawKSHp

* test(ui): stop the footer eating submit clicks on the long forms

The engagement and add-test forms run long enough that their button row sits
next to the page footer, and the footer wins the click. Selenium sometimes says
so (ElementClickInterceptedException naming #footer-wrapper) and sometimes the
click simply lands on the footer: the form is never submitted, the page never
changes, and the test fails much later asserting a success banner that was never
going to appear. A page-source dump taken at one such failure shows the browser
still sitting on a pristine "New Interactive Engagement" form -- no success
alert, no error alert, no errorlist.

click_submit() centres the button in the viewport before clicking it, which
takes it out from under the footer. Measured on threat_model_test.py, the file
that flaked most readily: 4 failures in 12 runs before, 0 in 12 after.

is_success_message_present() now waits for the banner instead of sampling once.
It only renders after the redirect that follows a POST, so the assertion was
riding on the 1s implicit wait -- fine locally, not always enough on a loaded
runner. Every caller asserts the banner is present and nothing asserts its
absence, so the wait costs nothing on the happy path and only delays a genuine
failure.

Both are applied to the two forms where interception was actually observed
rather than to every submit in the suite.

Two things deliberately NOT done. Targeting the "Done" button instead of the
first input.btn.btn-primary looks like the obvious tightening -- new_eng.html
renders "Import Scan Results", "Add Tests" then "Done", so the bare selector
submits the first one while the comment says Done -- but "Done" is the
bottom-right button, nearest the footer, and switching to it made
threat_model_test and calendar_test fail outright. That ordering is also
byte-identical in the template the classic UI used, so it is long-standing and
not a regression. And the footer overlap itself is left alone: #footer-wrapper
is position:static in normal flow and does not overlap the buttons at rest, so
this is a layout shift during load, not a stylesheet bug with an obvious fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rpv46XWjWXCWTQSkawKSHp

* test(ui): route the settings-form saves through click_submit too

The previous commit hardened the engagement and add-test forms against the
footer swallowing a submit click, and CI then failed in the one place it had
not covered:

    enable_false_positive_history -> change_system_setting
    AssertionError: False is not true   # assertTrue(is_enabled) after the save

Same mechanism, on the system settings form -- also long enough to put its save
button next to the footer. This one matters more than most: set_suite_settings()
calls change_system_setting() at the head of nearly every file in the suite, so
a click that lands on the footer there takes the whole file down before its
first real test. set_block_execution() and set_deduplication_execution_mode()
submit the same way on the profile form and get the same treatment.

The notification helpers submit that form too and are deliberately left alone.
notifications_test.py fails roughly one run in three locally with or without the
change -- on a different test each time -- so there is no signal to act on, and
it has passed every CI run so far. Patching it would have been guesswork.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rpv46XWjWXCWTQSkawKSHp

* test(ui): stop toggling false positive history while deduplication is on

The settings view refuses this combination outright:

    "Settings cannot be saved: Deduplicate findings and False positive history
     can not be set at the same time."

test_toggle_deduplication runs immediately before test_toggle_false_positive_history
and deliberately ends with deduplication ON, so the very next test asks for a
save the view will not perform, and change_system_setting()'s assertion fails.

This has been latent, not new: the save was already being refused, but the click
that submitted it was landing on the footer often enough that deduplication
never actually persisted, so the next test found it off and passed. Fixing the
swallowed click made the earlier test's effect stick, which is what surfaced
this. Confirmed against a running instance -- setting deduplication on and then
enabling false positive history through the UI produces exactly that warning and
no save.

So turn deduplication off around the false-positive-history toggle and restore
it afterwards, which keeps both contracts: the file still leaves deduplication
enabled for dedupe_test.py, and false positive history disabled as its default.

Verified with deduplication pre-set to on, the state CI reaches: all six files
in this group pass, and the settings land at deduplication on / false positive
history off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rpv46XWjWXCWTQSkawKSHp

* test(ui): centre the checkbox clicks too, not just the submits

A lost click on a checkbox is worse than a lost click on a submit button, and
until now only the submits were protected. When the footer eats a submit, the
form is simply never sent. When it eats a checkbox, the submit still happens and
the form saves with the checkbox in its old state, so the setting silently does
not change and the failure lands somewhere that looks unrelated.

That is what CI was showing. Once the submits stopped being lost, the failures
moved onto the checkboxes further down the same long form:

    enable_false_positive_history         -> assertTrue(is_enabled)   (earlier)
    enable_retroactive_false_positive_history -> assertTrue(is_enabled)

The settings view re-renders the bound form after a POST, so the checkbox in the
response reflects what was submitted, not what was saved. change_system_setting()
asserting False can therefore only mean the checkbox click never registered --
the save itself would have re-rendered it checked either way.

click_submit() is now a thin wrapper over a general click_centered(), used for
the setting toggles in change_system_setting() and set_block_execution() as well.

notifications_test.py gets the same treatment, and this reverses an earlier call.
I had left it alone after a single run suggested the change made things worse,
but that run was against a database dirtied by dozens of earlier files. On a
clean database -- what CI actually gives each group -- the unpatched file fails
at test_enable_personal_notification with the mail setting simply not applied,
which is the lost-click signature exactly. Patched, it passes 22/22.

Verified on a fresh database: all of groups 04 and 05 pass, plus product_test,
dedupe_test, close_old_findings_test and threat_model_test as the heaviest users
of the changed helper -- 290 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rpv46XWjWXCWTQSkawKSHp

* test(ui): route every form submit through click_submit()

Fixing these one at a time has cost a CI cycle per site. Each run surfaced the
next lost click a little further along -- the engagement submit, then the
settings save, then the false positive history toggle, then the retroactive
toggle, then the CI/CD engagement's Done button -- always the same cause, the
footer taking a click on a long form, only ever a different button.

So convert the whole class at once: all 89 form-submit clicks across 29 files.
The change is mechanical and semantically identical -- same selector, same
element, centred in the viewport before the click. The one time centring made
things worse was when it was paired with changing WHICH button was clicked
(targeting Done instead of the first primary submit, which moved the click
nearer the footer rather than away from it); that is not what this does.

The last CI run left group 02 failing on exactly this, in engagement_test.py,
where the submit is input[value='Done'] -- the bottom-right button and so the
most footer-exposed of the three.

Verified over the first 34 files of the UI matrix on a clean database, 0
failures, covering every group that has failed at any point (01, 02, 04, 05)
including engagement_test.py itself. The remaining files were still running when
this went out; they are all the same transformation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rpv46XWjWXCWTQSkawKSHp

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(psirt): new PSIRT section — advisory feeds, terms acceptance, clearance states

Documents the platform-native PSIRT advisory feeds shipping in Pro
(beta, behind the PSIRT feature flag + license entitlement): the
customer-self-fetch posture, the 20-source shipped catalog with its
cleared / pending-clearance states, the recorded terms-acceptance
transaction (append-only ledger, encrypted credentials), custom feeds,
feed health, and the attribution rendered with advisory content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(psirt): Import SBOM and Feed Findings pages

Documents the two surfaces that answer "am I vulnerable to this advisory?" — the
global SBOM upload that gets inventory in without visiting each product, and the
triage queue that gives each advisory its answer.

The Feed Findings page spends its length on the part users will otherwise get
wrong: the answer is three-valued, and "not affected" and "unknown" are different
claims. Reporting a component with no recorded version as "not affected" would
tell someone they are safe on the strength of missing data, so the docs say what
each state means and what the reader can do about an unknown. Same reasoning for
verified vs unverified matches, and for why a confirmed match is never retracted
automatically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(psirt): Matching Rules and Cases/SLA pages

Two surfaces that were shipping without documentation, and both need it more
than the pages already here — each has a behaviour a user will otherwise read
as a bug.

**Matching Rules** explains why a rule can be refused. A rule graded weak
cannot be enabled, and someone who hits that without knowing the rule exists
will conclude the feature is broken rather than that their rule matches every
advisory in the store. The page leads with the grading, states the two-sided
requirement with a worked example, and covers group scope, opt-out precedence,
templates, and why deleting a group switches its rules off.

**Cases and SLA** documents the grouping suggestions and the clock. The parts
that need saying out loud:

* a breach never clears — work finished late stops as breached, not met;
* the triage clock cannot be paused at all, and why;
* a paused clock's deadline moves, so paused time is not charged to the team;
* the tier is fixed when the clock starts and can only ever move up;
* mitigation priority is not the SLA tier, and downgrading one does not move a
  deadline;
* closing a case does not stop a clock, because the obligation belongs to the
  advisories inside it;
* you do not need a case at all to get a finding into DefectDojo.

Also reordered the section so the sidebar matches the order the entries appear
in the app menu — they disagreed, which would have had a reader following two
different sequences for the same feature — and added a "how it fits together"
list to the section index, including that the workflow pieces are optional.

* docs(psirt): advisory publishing and the PSIRT dashboard

Two pages for the halves of PSIRT that were undocumented, plus the index entries
that place them.

`advisories.md` covers the direction nothing else in PSIRT does: publishing your
own advisories rather than consuming other people's. The parts a reader needs
before they use it, not after — that editing approved content costs you the
approvals (and why that is not a setting), that a rejection needs a reason, that
preflight reports un-built checks as pending rather than passing, that exclusions
beat both derived and added recipients, and that `skipped_not_configured` and
`not_implemented` are neither success nor failure.

`dashboard.md` leads with the four-way exposure split and says plainly that
"not affected" and "no signal" are not the same claim, because that is the
misreading the whole design exists to prevent. Also why feed health and triage SLA
sit side by side — an SLA that looks healthy while a feed has silently stopped
polling is the worst combination, and only visible together — and why feed health
declines to say "all clear".

The index now reads as seven steps with an explicit note that steps 1-5 are about
what others published and step 6 is the other direction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(psirt): components, SLA policies, settings, and the relevance surfaces

Three new pages for surfaces that previously had no documentation, and
which until now could only be configured through the Django admin:

* Components — the SBOM inventory as PSIRT reads it, one row per
  (component, asset), and the per-pair judgement you add: a PCRSS
  rating, an authoritative CPE (the strongest correlation axis, which a
  purl-keyed SBOM row cannot carry), and tags. Says plainly that a
  versionless row can only ever be answered "unknown".
* SLA Policies — tuning the tier ladder. Separates the clock definition
  (the obligation, decided in code) from the tiers (the calibration you
  own), and explains why editing a tier never moves a running clock and
  why deleting one that live clocks use is refused.
* PSIRT Settings — case-worthiness weights and bands, the "new" item
  window, and the material-change policy, including why KEV-add is on by
  default and KEV-remove is not.

Matching Rules gains the four capabilities from the relevance pipeline:
building rules from an inventory, previewing a rule or an unsaved
condition set, per-rule effectiveness with the dead vs insufficient-data
distinction, and per-asset coverage with the inventory tempering that
stops "covered" being earned by one precise rule beside a thousand
unwatched components.

Index updated and the chapter renumbered so the reading order matches
the workflow.

---------

Co-authored-by: Greg Anderson <greg@Gregs-MacBook-Pro.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…Dojo#14932)

* API token revocation

* Correct TokenAuthentication import and add expiry to classic template

* Ruff linter compliance for token expiry feature

* Enhance API token management documentation and improve token expiry error message

* feat(api-tokens): wire model field, URL registration, and unit tests

- Add token_expiry DateTimeField to UserContactInfo (was missing from
  initial commit despite migration referencing it)
- Register ApiTokenViewSet at api-tokens/ in urls.py (was missing from
  initial commit despite ViewSet existing in views.py)
- Add unit tests for list, retrieve, revoke, expiry enforcement, and
  default-expiry-on-reset behaviours

* Ruff compliance and docs correction

* Fix silent pass in API test

* refactor: replace proposed token viewset with simple revoke-by-key endpoint

* docs: update token management section after change to revocation and expiry processes

* fix(lint): hoist token expiry imports to module scope

The serializer imported Token and token_expires_at inside get_token_expiry to
sidestep a circular import that does not exist: dojo.user.authentication reaches
only authorization, models and notifications.helper, none of which import back
into dojo.user.api.serializer, and dojo/user/api/views.py already imports both
modules at module scope. Resolves PLC0415 (import-outside-top-level), which was
gating the whole unit-test suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tests): keep the admin token fresh, pin the retroactive behaviour, drop a Docsy shortcode

The six failures were all in the new test file; the rest of the suite (6491 tests)
was unaffected.

Five of them failed in setup with 403 "API token has expired." rather than at
their assertion: dojo_testdata's admin token has an old created timestamp, so any
test switching on a default lifetime expired the very credential used to build
its fixtures. The admin token is now stamped fresh in setUp.

That collision is worth stating rather than papering over, so
test_enabling_the_default_expires_pre_existing_tokens now pins it directly: a
200-day-old token works, and the moment a 90-day default is switched on the same
token is rejected. Evaluating expiry at use is what makes the control impossible
to sidestep, and retroactivity is the price. The docs now carry that warning,
along with the recovery path (the UI uses session auth and can issue a new token).

The sixth was a real test defect: a disabled form field falls back to its initial
value through a round trip that drops microseconds, so comparing exact datetimes
failed on precision while the property under test held. It now asserts the posted
value was ignored, to the second.

Also replaces the {{% alert %}} block added with the earlier docs edit. That is a
Docsy shortcode; this site runs Thulite Doks, where {{< highlight >}} is the only
shortcode in use across docs/content, so it would not have rendered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Greg Anderson <greg.anderson@owasp.org>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
devGregA and others added 15 commits August 19, 2026 17:10
…#15673)

Documents granting a role against an Organization *type* rather than one Organization at a
time, so it applies to every Organization of that type and to ones created later.

Extends the existing Organizations page rather than adding one: Organization Types and
union-of-grants already ship there, so the new section cross-links them instead of
restating them. Covers what the grant does and does not do (bounded by the role, follows
membership, additive-only, ignores nesting), the deployment flag, and why managing grants
is restricted to superusers and global owners.

Co-authored-by: devGregA <greg-agent-2@defectdojo.com>
…on (DefectDojo#15646)

A connector whose runs fail, or whose tool stops returning records, is
silent outside the connectors page. Document the notification that tells
you, what each message covers, and how often it arrives.
…15628)

Co-authored-by: devGregA <greg-agent-2@defectdojo.com>
…fectdojo/chart.yaml) (DefectDojo#15721)

* chore(deps): update valkey docker tag from 0.25.4 to v0.25.5 (helm/defectdojo/chart.yaml)

* update Helm documentation

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* docs: asset exposure and deployment context

Documents the Asset Exposure beta feature in DefectDojo Pro: the five exposure
verdicts and how conflicting sources resolve, why automated sources may report
exposure but never assert isolation, the per-asset override, deployment context
from environment mapping and a production branch, how both signals adjust
priority, the optional risk floor and ceiling, and the separate opt-in for using
exposure in VDR remediation tiers.

Also cross-links the existing Reachability page, since the two answer different
questions (code reachable inside the app, versus asset reachable from outside)
and are easy to confuse.

* docs: exposure inherited from where an asset runs

An asset can be reachable without anything having scanned it, because of what it is
deployed onto. Documents the deploys-to relationship, the effective-exposure badge,
and the two limits that will otherwise surprise people: inherited exposure does not
move priority, and you only inherit from assets your permissions let you see.

Folded into this page rather than a new one because it is the same question the page
already answers, and a reader looking up "is this asset reachable" should not have to
know that the answer is split across two documents.

* docs: universal parser can map asset exposure

Adds the Asset exposure row to the mappable-fields table, plus the Reachability row,
which has been mappable for a while and was never listed -- a field users can map but
cannot discover is not much of a feature.

Notes the one thing about Asset exposure that will otherwise confuse people: it is the
only output field describing something other than the finding, so every row mapping it
asserts the same thing about the same asset. That is expected and costs nothing.

* docs: fix broken internal link to asset exposure page

The relative link resolved under import_data/ instead of the site root,
failing the lychee internal-link check. Use an absolute path matching the
docs convention for cross-section links.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: devGregA <greg-agent-2@defectdojo.com>
Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…1.38.2 to v1.38.3 (helm/defectdojo/values.yaml) (DefectDojo#15718)

* chore(deps): update gcr.io/cloudsql-docker/gce-proxy docker tag from 1.38.2 to v1.38.3 (helm/defectdojo/values.yaml)

* update Helm documentation

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
….5.1 to v1.5.3 (helm/defectdojo/values.yaml) (DefectDojo#15720)

* chore(deps): update nginx/nginx-prometheus-exporter docker tag from 1.5.1 to v1.5.3 (helm/defectdojo/values.yaml)

* update Helm documentation

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com>
….1 to 9.1.1-alpine (docker-compose.yml) (DefectDojo#15744)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…#15756)

* docs(connectors): restore three markdown links mangled into NUL bytes

`docs/content/connectors/upstream/toolreference.md` carried six literal NUL
bytes (0x00) that broke three inline markdown links. Each one is a leftover
placeholder of the form `\0<index>\0` sitting where the link destination
`](url)` belongs:

- Semgrep token page: `[https://semgrep.dev/orgs/\-/settings/tokens\0389\0)`
- Snyk REST base:     `**[https://api.snyk.io/rest\0394\0**`
- Snyk EU REST base:  `**[https://api.eu.snyk.io/rest\0395\0**`

The NUL bytes also made the whole file register as binary, so `grep` skipped
it and `git diff` rendered it as "Binary files differ".

The placeholders were introduced in 1cafb09 (DefectDojo#15661); its parent
1e48e93 still has the intact text, and all four translated siblings
(`.de`, `.es`, `.fr`, `.ja`) carry the same markup unchanged. Both sources
agree, so the destinations are restored verbatim from them - the two repaired
lines are now byte-identical to their pre-DefectDojo#15661 counterparts.

The file contains no NUL bytes after this change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: restore five more markdown links mangled into NUL bytes

Same defect as the previous commit, same origin (1cafb09 / DefectDojo#15661): a
placeholder of the form \0<index>\0 left sitting where a link destination
`](url)` belongs. A repo-wide scan found four more affected files carrying
ten NUL bytes between them:

- get_started/about/about_defectdojo.md          (2: demo, pro demo)
- get_started/contributing/documentation.md      (1: http://localhost:1313)
- get_started/contributing/how-to-write-a-parser.md (1: acunetix.md example)
- issue_tracking/pro_integration/messaging_connectors.md (1: api.slack.com/apps)

Each destination is restored verbatim from the last revision of that file
before DefectDojo#15661, located by walking the file history and counting NUL bytes per
revision. All five are self-links (display text equals destination), and every
repaired line is byte-identical to its pre-DefectDojo#15661 counterpart.

No NUL bytes remain anywhere under docs/, and a regex sweep for markdown link
openers with no `](` destination now reports zero hits across docs/content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…efectDojo#15755)

The GitHub Advanced Security connector can now import a repository's issue
tracker as a fourth finding type, GitHub:Issues, alongside the code scanning,
Dependabot and secret scanning alert families.

Documents the two settings that drive it -- Issue Labels as the filter, Issue
Severity Labels as the label-to-severity map, and Default Issue Severity for
whatever the map does not match -- plus the token permission issues need, which
is less than the alert families require and does not depend on Advanced
Security being enabled at all.

Adds a "what to expect" section covering the parts that surprise people: pull
requests are never imported even though GitHub returns them from the issues
endpoint, closing an issue closes the finding, the issue body is reproduced
verbatim as the description, and these findings carry no CWE, CVE or component
so they will not deduplicate against scanner findings for the same problem.

English only; the translated tool references are updated by their own pass.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ord (DefectDojo#15752)

The Wiz connector used to make a Record only for each Wiz Project, so a
tenant with no Projects had nothing to map and imported nothing. It now
always reports a tenant-level Record as well, named after the tenant.

Rewrite the Wiz section to say what the connector imports and what a
Record is, the same way the other tool sections do. Drop the note saying
an account without Project visibility has nothing to import, because that
is no longer true, and explain when to map the tenant-level Record.
Translations updated to match.
DefectDojo#15761)

* docs: 3.3.x upgrade notes for the three deduplication identity changes

The 3.3 upgrade page said "no special instructions", which this release makes wrong three
times over: Xeol Parser, Checkmarx One Scan and Checkmarx Scan detailed all gain
HASHCODE_FIELDS_PER_SCANNER registrations, so findings imported before the upgrade carry
hashes that no longer match what an import computes after it.

Follows the shape the 3.2 notes set: what changed and why per scan type, then two paths.
Instances running the identity signature ledger (default from Pro 3.2.300) need no action
because drift detection, the scoped rehash and signature matching bridge the change; the
one caveat spelled out is that a large never-backfilled instance should run the backfill
before upgrading, since a rehash replaces the identities the backfill exists to record.
Everyone else gets the three manage.py dedupe commands in the same form the 2.4x and 3.2
notes use.

* docs: the drift watch reports, it does not repair

The Pro section overpromised twice. It claimed both ledger flags default on in 3.2.300,
and matching is deliberately opt-in. It also claimed the definition change is repaired
automatically, but identity_drift_watch notifies and stops there; the repair is the
operator accepting the change or running the same dedupe commands everyone else uses.

Rewritten so the Pro path is honest: recording is on by default, the drift watch will
tell you, and the fix is the same three commands or a Tuner acceptance. The
backfill-before-upgrade guidance stays, scoped to instances where matching is enabled,
because that is where the preserved identities are actually consulted.
@github-actions github-actions Bot added docker New Migration Adding a new migration file. Take care when merging. settings_changes Needs changes to settings.py based on changes in settings.dist.py included in this PR apiv2 docs unittests integration_tests ui parser helm localization labels Aug 23, 2026
@koushik-hs

Copy link
Copy Markdown
Author

I just noticed that this issue was already addressed upstream in an earlier PR. I missed that discussion before working on this. Closing this PR to avoid duplicating the existing fix. Sorry for the noise!

@koushik-hs koushik-hs closed this Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

apiv2 docker docs helm integration_tests localization New Migration Adding a new migration file. Take care when merging. parser settings_changes Needs changes to settings.py based on changes in settings.dist.py included in this PR ui unittests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Login: #toggleBox position:absolute;left:0 breaks layout in Chrome (3.0.100)

8 participants