Skip to content

feat(locations): accept legacy writes on /api/v2/endpoints and /endpoint_status - #16404

Open
devGregA wants to merge 2 commits into
DefectDojo:devfrom
devGregA:feat/locations-legacy-endpoint-write-compat
Open

devGregA wants to merge 2 commits into
DefectDojo:devfrom
devGregA:feat/locations-legacy-endpoint-write-compat

Conversation

@devGregA

Copy link
Copy Markdown
Contributor

Summary

With V3_FEATURE_LOCATIONS on, /api/v2/endpoints/ and /api/v2/endpoint_status/ are served from Locations, but every write answered 403. Automation written against the Endpoint API could read its endpoints and statuses and change none of them. This makes those routes accept the legacy payloads and apply them to the Location rows they stand for.

An endpoint row is one product's LocationProductReference to a URL Location, and an endpoint status row is a LocationFindingReference. A Location is shared by every product that recorded the same URL, which an Endpoint never was, so each write acts on one product's rows and leaves other products' rows for the same URL alone.

/api/v2/endpoints/

  • POST records the URL on the product, reusing the Location if another product already has it. A duplicate for the same product answers the legacy 400.
  • PUT/PATCH change the address. If only this product uses the Location and the new address is new, the URL changes in place and keeps its id, tags and metadata. Otherwise this product's reference, its findings' references and its location metadata move to the Location for the new address. product cannot change.
  • DELETE removes this product's reference, its findings' references and its location metadata, and the Location when nothing else uses it (remove_location_references).
  • Tags: a Location has one tag set, so a tag write is allowed only when the caller can edit every product referencing the Location; otherwise 400 and nothing changes.

/api/v2/endpoint_status/

  • POST links the finding to the endpoint (same product required, as before).
  • PUT/PATCH map the four flags onto the single status: a flag set true in the request wins, several at once resolve in the backfill's order (risk accepted, false positive, out of scope, mitigated), and clearing the current flag reactivates the row. A status change records the auditor and time; mitigated_by names the auditor for Mitigated. The product reference's status is recomputed after each change.
  • DELETE unlinks the finding and keeps the endpoint.

Permissions mirror the legacy classes (add/edit/delete on the endpoint's product, edit on the status row's finding). Writes run under a pghistory.context(source="endpoint_api_compat"). The logic lives in dojo/location/services.py, so the serializers only translate payloads.

Read fixes on the same routes

  • The status row's endpoint field answered the Location id. It now answers the id /api/v2/endpoints/ shows (the finding's product's reference), so the two routes join again; the value comes from a subquery instead of one query per row.
  • ?endpoint= matched the shared Location's statuses in every product. It now returns that product's statuses only.
  • POST /api/v2/endpoints/{id}/generate_report/ answered 400 (Report cannot be generated for object of type LocationProductReference). It now reports on the product's findings on that host, and the report's endpoint uses the compat serializer.
  • The 410 sunset message no longer says writes are unavailable.

Second commit: a finding's endpoints field. With Locations on, endpoints on /api/v2/findings/ listed the finding's LocationFindingReference ids, and writing it removed and re-created every location reference on the finding, resetting their statuses and dropping code and dependency locations. It now lists, and accepts, the /api/v2/endpoints/ ids; a write keeps the status of the endpoints it keeps and only touches URL references. ?prefetch=endpoints follows the same ids. This changes the field's value for clients that adapted to the reference ids, which is why it is a separate commit: it can be dropped without touching the rest.

Docs: "Migrating from Endpoints" now describes reads, writes, the flag mapping and the remaining differences.

Behaviour change to review

test_apiv2_endpoint_deprecation_contract pinned writes at 403 and said changing that is a decision for a minor release, so this is milestoned for 3.5.0.

Tests

  • V3EndpointTest and V3EndpointStatusTest in unittests.test_rest_framework now run the shared CRUD suite (create, update, delete, delete preview, object-permission checks, schema checks) against the compat viewsets, plus the legacy EndpointStatusTest cases ported to the Locations fixture.
  • unittests.test_v3_endpoint_compat_writes covers the shared-Location behaviour, tag writes and their rollback, the flag mapping, product status sync, generate_report, the pghistory source and the endpoint read fix.
  • unittests.test_v3_finding_endpoints_field covers the finding field; test_apiv2_prefetch_locations, test_v3_location_reference_authz and FindingsTest follow the new ids.
  • With the first commit's source files reverted to dev, 34 of the 49 tests it adds or changes fail; the 15 that pass are the unchanged reads, the not-authorized guards and the 410 handler test. With the second commit's source reverted, 3 of its 4 new tests fail; the fourth checks that a PATCH without the field leaves references alone.
  • Full suite locally, both DD_V3_FEATURE_LOCATIONS values, the CI phases of docker/entrypoint-unit-tests.sh: parallel phase 8240 tests OK each; non-parallel, transactional and performance phases OK; makemigrations --check and spectacular --fail-on-warn clean; ruff 0.16.10 clean.

devGregA and others added 2 commits October 18, 2026 15:24
…int_status

With V3_FEATURE_LOCATIONS on, the two Endpoint routes are served from
Locations, and every POST, PUT, PATCH and DELETE answered 403. Automation
written against the Endpoint API could read its endpoints and statuses but
change none of them.

The compat viewsets now accept the legacy payloads and apply them to the
Location rows they stand for. An endpoint is one product's
LocationProductReference to a URL Location; an endpoint status is a
LocationFindingReference. A Location is shared by every product that
recorded the URL, so each write acts on one product's rows only:

- endpoints: create reuses an existing Location; an address change edits
  the URL in place when only this product uses it, and otherwise moves
  this product's reference, finding references and location metadata to
  the Location for the new address; delete removes this product's rows and
  the Location once nothing uses it. Tag writes need edit on every product
  referencing the Location.
- endpoint_status: create links the finding (same product, as before);
  the four flags map onto the single status (a flag set true in the request
  wins, several resolve in the backfill's order, clearing the set flag
  reactivates the row); status changes record auditor and time, and
  mitigated_by names the auditor; the product reference's status is
  recomputed.

Permissions mirror UserHasEndpointPermission and
UserHasEndpointStatusPermission. Writes run in a
pghistory.context(source="endpoint_api_compat"). The logic is in
dojo/location/services.py.

Also on these routes:
- endpoint_status.endpoint answered the Location id; it now answers the id
  /api/v2/endpoints/ shows for the row, from a subquery rather than a query
  per row.
- ?endpoint= matched the shared Location's statuses in every product; it
  now returns that product's statuses only.
- generate_report on an endpoint answered 400 ("Report cannot be generated
  for object of type LocationProductReference"); it now reports on the
  product's findings on that host.
- The 410 sunset message no longer says writes are unavailable.

V3EndpointTest and V3EndpointStatusTest now run the shared CRUD suite
against the compat viewsets, with the legacy EndpointStatusTest cases ported
to the Locations fixture.

Reverting the source files of this commit to dev while keeping its tests
fails 34 of the 49 tests it adds or changes; the 15 that pass either way
are the unchanged list/detail/prefetch reads, the not-authorized guards and
the 410 handler unit test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Locations on, the endpoints field of /api/v2/findings/ listed the
finding's LocationFindingReference ids, so a client following them to
/api/v2/endpoints/{id}/ found the wrong row or none. Writing the field took
LocationFindingReference ids too, then removed and re-created every location
reference on the finding: statuses were reset to the finding's status, and
code and dependency locations were dropped along with the URLs.

The field now reads and accepts the ids /api/v2/endpoints/ shows (the
finding's product's references to the URL Locations the finding is on). A
write keeps the status of the endpoints it keeps, unlinks the ones it drops,
links new ones, and touches URL references only. ?prefetch=endpoints follows
the same ids, and the findings list prefetches the product references so the
field costs no query per row.

This changes the field's value for clients that adapted to the reference
ids, so it is its own commit.

Reverting the serializer, view and prefetcher changes fails 3 of the 4 new
tests; the fourth checks that a PATCH without the field leaves the
references alone, which held before too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant