diff --git a/pkg/cookiebanner/tracker_mapping_worker.go b/pkg/cookiebanner/tracker_mapping_worker.go index e582d9899..fd151a2a8 100644 --- a/pkg/cookiebanner/tracker_mapping_worker.go +++ b/pkg/cookiebanner/tracker_mapping_worker.go @@ -215,6 +215,7 @@ func (h *trackerMappingHandler) Process(ctx context.Context, tp coredata.Tracker commonPatternID := det.commonPatternID commonThirdPartyID := det.commonThirdPartyID directThirdPartyID := det.directThirdPartyID + firstParty := det.firstParty // Phase 2: tracker-mapping agent (no transaction). It runs only when // the deterministic signals could not resolve a catalog third party. @@ -251,6 +252,7 @@ func (h *trackerMappingHandler) Process(ctx context.Context, tp coredata.Tracker commonPatternID = firstNonNil(commonPatternID, match.commonPatternID) commonThirdPartyID = match.commonThirdPartyID + firstParty = match.firstParty return nil }, @@ -265,7 +267,12 @@ func (h *trackerMappingHandler) Process(ctx context.Context, tp coredata.Tracker // touches the database (in a short transaction). thirdPartyID := tp.ThirdPartyID - if thirdPartyID == nil { + // A first-party verdict is terminal: the artifact has no vendor, so + // any org ThirdParty link a prior mapping run left on the pattern is + // stale and must be cleared. + if firstParty { + thirdPartyID = nil + } else if thirdPartyID == nil { switch { case directThirdPartyID != nil: thirdPartyID = directThirdPartyID diff --git a/pkg/coredata/common_tracker_pattern.go b/pkg/coredata/common_tracker_pattern.go index 812f84075..5dcc3fbc7 100644 --- a/pkg/coredata/common_tracker_pattern.go +++ b/pkg/coredata/common_tracker_pattern.go @@ -309,16 +309,20 @@ SET -- re-queued for enrichment: the enrichment agent leaves descriptions -- blank when it cannot substantiate a purpose, and knowing the vendor -- gives it a second, better-informed attempt. The attempt counter is - -- reset so the re-armed row gets a fresh retry budget. + -- reset so the re-armed row gets a fresh retry budget. A terminal + -- FIRST_PARTY row never gains a vendor (the clause above discards the + -- incoming one), so it must never re-arm on a vendor it did not adopt. enrichment_requested_at = CASE - WHEN common_tracker_patterns.description = '' + WHEN common_tracker_patterns.attribution <> 'FIRST_PARTY' + AND common_tracker_patterns.description = '' AND common_tracker_patterns.common_third_party_id IS NULL AND EXCLUDED.common_third_party_id IS NOT NULL THEN NOW() ELSE common_tracker_patterns.enrichment_requested_at END, enrichment_attempts = CASE - WHEN common_tracker_patterns.description = '' + WHEN common_tracker_patterns.attribution <> 'FIRST_PARTY' + AND common_tracker_patterns.description = '' AND common_tracker_patterns.common_third_party_id IS NULL AND EXCLUDED.common_third_party_id IS NOT NULL THEN 0 @@ -330,7 +334,8 @@ SET -- unrecoverable: the stale-recovery sweep skips any row whose payload -- is non-null, so the re-claimed-but-never-finished row never requeues. enrichment = CASE - WHEN common_tracker_patterns.description = '' + WHEN common_tracker_patterns.attribution <> 'FIRST_PARTY' + AND common_tracker_patterns.description = '' AND common_tracker_patterns.common_third_party_id IS NULL AND EXCLUDED.common_third_party_id IS NOT NULL THEN NULL diff --git a/pkg/proboctl/commontrackerpattern/upsert.go b/pkg/proboctl/commontrackerpattern/upsert.go index 643dba378..730b7ea15 100644 --- a/pkg/proboctl/commontrackerpattern/upsert.go +++ b/pkg/proboctl/commontrackerpattern/upsert.go @@ -158,6 +158,18 @@ func newCmdUpsert(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("common-third-party") { if flagCommonThirdParty == "" { pattern.CommonThirdPartyID = nil + + // Removing the vendor invalidates a THIRD_PARTY + // verdict, which by definition carries one. When the + // operator did not set an explicit --attribution, + // downgrade the now-stale verdict to UNDETERMINED so + // the mapping pipeline probes the vendor-free row + // again. A FIRST_PARTY row is already vendor-free and + // terminal, so it is left untouched. + if !cmd.Flags().Changed("attribution") && + pattern.Attribution == coredata.CommonTrackerPatternAttributionThirdParty { + pattern.Attribution = coredata.CommonTrackerPatternAttributionUndetermined + } } else { thirdPartyID, err := resolveCommonThirdPartyID(ctx, tx, flagCommonThirdParty) if err != nil { @@ -165,6 +177,19 @@ func newCmdUpsert(f *cmdutil.Factory) *cobra.Command { } pattern.CommonThirdPartyID = &thirdPartyID + + // A vendor-linked row is, by definition, attributed to + // a third party. When the operator did not set an + // explicit --attribution, normalize the verdict to + // THIRD_PARTY so the row never persists with an + // UNDETERMINED (or unset) verdict. A FIRST_PARTY row is + // terminal and stays vendor-free — the upsert discards + // the incoming vendor and keeps the verdict — so it is + // left untouched. + if !cmd.Flags().Changed("attribution") && + pattern.Attribution != coredata.CommonTrackerPatternAttributionFirstParty { + pattern.Attribution = coredata.CommonTrackerPatternAttributionThirdParty + } } }