The source headers, LICENSE files, and license metadata had drifted apart. Align the entire project to MIT: - Convert every source-file header to the MIT text across all comment styles (Go, TS, TSX, JS, MJS, SQL, CSS, GraphQL, shell), including SPDX-License-Identifier tags - Set the root and cookie-banner LICENSE files to the MIT text with a "MIT License" title line - Switch the package.json license fields, Docker image label, and cookie-banner README to MIT - Update docs and the genmodels header generator accordingly - Normalize copyright lines to a single format (Copyright (c) <year(s)> Probo Inc <hello@probo.com>.): unify the hello@getprobo.com and hello@probo.inc emails to hello@probo.com and the comma-separated years to a hyphenated range Genuine third-party references are intentionally left untouched: the Lucide icon attributions (Lucide is ISC) and the trivy dependency license allowlist. Signed-off-by: Sacha Al Himdani <sacha@probo.com>
252 lines
8.9 KiB
Go
252 lines
8.9 KiB
Go
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
|
|
package commontrackerpattern
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"go.gearno.de/kit/pg"
|
|
"go.probo.inc/probo/pkg/coredata"
|
|
"go.probo.inc/probo/pkg/gid"
|
|
"go.probo.inc/probo/pkg/proboctl/cmdutil"
|
|
)
|
|
|
|
func newCmdUpsert(f *cmdutil.Factory) *cobra.Command {
|
|
var (
|
|
flagTrackerType string
|
|
flagPattern string
|
|
flagMatchType string
|
|
flagDescription string
|
|
flagMaxAgeSeconds int
|
|
flagConfidence float32
|
|
flagAttribution string
|
|
flagCommonThirdParty string
|
|
flagDryRun bool
|
|
flagEnrich bool
|
|
)
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "upsert",
|
|
Short: "Create or update a common tracker pattern in the global catalog",
|
|
Long: "Insert a new common tracker pattern or update an existing one keyed by " +
|
|
"its natural key (--tracker-type, --pattern, --max-age-seconds). Only " +
|
|
"--tracker-type and --pattern are required; every other field is updated " +
|
|
"only when its flag is passed, so an existing row's other columns are " +
|
|
"preserved. An empty --description never overwrites an existing one — " +
|
|
"descriptions are owned by the enrichment worker. A FIRST_PARTY row stays " +
|
|
"vendor-free and keeps its terminal verdict regardless of --common-third-party " +
|
|
"or --attribution.\n\n" +
|
|
"Pass --enrich to queue the row for the async enrichment worker after " +
|
|
"writing, so a minimal pattern row gets its description (and vendor " +
|
|
"attribution) researched. Enrichment is expensive (LLM per row).",
|
|
Args: cobra.NoArgs,
|
|
}
|
|
|
|
cmd.Flags().StringVar(&flagTrackerType, "tracker-type", "", "Tracker type (required)")
|
|
cmd.Flags().StringVar(&flagPattern, "pattern", "", "Pattern key (required)")
|
|
cmd.Flags().StringVar(&flagMatchType, "match-type", string(coredata.TrackerPatternMatchTypeExact), "Match type (EXACT, PREFIX, GLOB)")
|
|
cmd.Flags().StringVar(&flagDescription, "description", "", "Human-readable description")
|
|
cmd.Flags().IntVar(&flagMaxAgeSeconds, "max-age-seconds", 0, "Cookie max-age in seconds (part of the natural key)")
|
|
cmd.Flags().Float32Var(&flagConfidence, "confidence", 1.0, "Confidence score (default: 1.0 for a manual entry)")
|
|
cmd.Flags().StringVar(&flagAttribution, "attribution", "", "Attribution verdict (UNDETERMINED, THIRD_PARTY, FIRST_PARTY)")
|
|
cmd.Flags().StringVar(&flagCommonThirdParty, "common-third-party", "", "Common third party to link (slug or GID); pass empty to unlink")
|
|
cmd.Flags().BoolVar(&flagDryRun, "dry-run", false, "Print the resulting row without writing")
|
|
cmd.Flags().BoolVar(&flagEnrich, "enrich", false, "Queue the row for the async enrichment worker after writing")
|
|
|
|
_ = cmd.MarkFlagRequired("tracker-type")
|
|
_ = cmd.MarkFlagRequired("pattern")
|
|
|
|
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
|
ctx := cmd.Context()
|
|
|
|
trackerType := coredata.TrackerType(flagTrackerType)
|
|
if !trackerType.IsValid() {
|
|
return fmt.Errorf("invalid --tracker-type value %q", flagTrackerType)
|
|
}
|
|
|
|
matchType := coredata.TrackerPatternMatchType(flagMatchType)
|
|
if !matchType.IsValid() {
|
|
return fmt.Errorf("invalid --match-type value %q", flagMatchType)
|
|
}
|
|
|
|
if flagPattern == "" {
|
|
return fmt.Errorf("--pattern must not be empty")
|
|
}
|
|
|
|
var attribution coredata.CommonTrackerPatternAttribution
|
|
if cmd.Flags().Changed("attribution") {
|
|
attribution = coredata.CommonTrackerPatternAttribution(flagAttribution)
|
|
if !attribution.IsValid() {
|
|
return fmt.Errorf("invalid --attribution value %q", flagAttribution)
|
|
}
|
|
}
|
|
|
|
// max_age_seconds is part of the natural key, so an unset flag means
|
|
// "the row with no max age" rather than "leave it untouched".
|
|
var maxAge *int
|
|
if cmd.Flags().Changed("max-age-seconds") {
|
|
maxAge = &flagMaxAgeSeconds
|
|
}
|
|
|
|
pgClient, err := f.PgClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
out := f.IOStreams.Out
|
|
|
|
var (
|
|
pattern coredata.CommonTrackerPattern
|
|
inserted bool
|
|
)
|
|
|
|
if err := pgClient.WithTx(
|
|
ctx,
|
|
func(ctx context.Context, tx pg.Tx) error {
|
|
now := time.Now()
|
|
|
|
existing := coredata.CommonTrackerPattern{}
|
|
|
|
err := existing.LoadByPattern(ctx, tx, trackerType, flagPattern, maxAge)
|
|
switch {
|
|
case err == nil:
|
|
pattern = existing
|
|
case errors.Is(err, coredata.ErrResourceNotFound):
|
|
pattern = coredata.CommonTrackerPattern{
|
|
ID: gid.New(gid.NilTenant, coredata.CommonTrackerPatternEntityType),
|
|
Confidence: flagConfidence,
|
|
CreatedAt: now,
|
|
}
|
|
default:
|
|
return fmt.Errorf("cannot load common tracker pattern by pattern: %w", err)
|
|
}
|
|
|
|
pattern.TrackerType = trackerType
|
|
pattern.Pattern = flagPattern
|
|
pattern.MatchType = matchType
|
|
pattern.MaxAgeSeconds = maxAge
|
|
pattern.UpdatedAt = now
|
|
|
|
if cmd.Flags().Changed("description") {
|
|
pattern.Description = flagDescription
|
|
}
|
|
|
|
if cmd.Flags().Changed("confidence") {
|
|
pattern.Confidence = flagConfidence
|
|
}
|
|
|
|
if cmd.Flags().Changed("attribution") {
|
|
pattern.Attribution = attribution
|
|
}
|
|
|
|
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 {
|
|
return err
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
if flagDryRun {
|
|
return nil
|
|
}
|
|
|
|
inserted, err = pattern.Upsert(ctx, tx)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot upsert common tracker pattern: %w", err)
|
|
}
|
|
|
|
// Arm enrichment explicitly rather than via the receiver:
|
|
// Upsert only queues a description-less row on insert and
|
|
// otherwise leaves the enrichment columns untouched, so this
|
|
// is the only path that re-arms an existing row uniformly. It
|
|
// also resets the attempt budget for a fresh run.
|
|
if flagEnrich {
|
|
var patterns coredata.CommonTrackerPatterns
|
|
|
|
if _, err := patterns.RequestEnrichmentByIDs(ctx, tx, []gid.GID{pattern.ID}); err != nil {
|
|
return fmt.Errorf("cannot queue enrichment: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
enrichSuffix := ""
|
|
if flagEnrich {
|
|
enrichSuffix = " (queued for enrichment)"
|
|
}
|
|
|
|
if flagDryRun {
|
|
_, _ = fmt.Fprintf(out, "Would upsert common tracker pattern %q (tracker type %s, match type %s)%s.\n", pattern.Pattern, pattern.TrackerType, pattern.MatchType, enrichSuffix)
|
|
return nil
|
|
}
|
|
|
|
action := "Updated"
|
|
if inserted {
|
|
action = "Created"
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(out, "%s common tracker pattern %s (%q, tracker type %s)%s.\n", action, pattern.ID.String(), pattern.Pattern, pattern.TrackerType, enrichSuffix)
|
|
|
|
return nil
|
|
}
|
|
|
|
return cmd
|
|
}
|