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>
257 lines
7.2 KiB
Go
257 lines
7.2 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"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/spf13/cobra"
|
|
"go.gearno.de/kit/pg"
|
|
clicmdutil "go.probo.inc/probo/pkg/cmd/cmdutil"
|
|
"go.probo.inc/probo/pkg/coredata"
|
|
"go.probo.inc/probo/pkg/gid"
|
|
"go.probo.inc/probo/pkg/proboctl/cmdutil"
|
|
)
|
|
|
|
// patternEnrichmentMetadataView mirrors the subset of the common tracker
|
|
// pattern enrichment payload (written by the enrichment worker) that show
|
|
// renders. It is decoded locally to avoid a dependency on the cookiebanner
|
|
// package.
|
|
type patternEnrichmentMetadataView struct {
|
|
Model string `json:"model"`
|
|
AttemptedAt time.Time `json:"attempted_at"`
|
|
Status string `json:"status"`
|
|
Error string `json:"error"`
|
|
Fields map[string]patternEnrichmentFieldView `json:"fields"`
|
|
Attribution *patternEnrichmentAttributionView `json:"attribution"`
|
|
}
|
|
|
|
type patternEnrichmentFieldView struct {
|
|
Status string `json:"status"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type patternEnrichmentAttributionView struct {
|
|
ThirdPartyName string `json:"third_party_name"`
|
|
Category string `json:"category"`
|
|
Confidence float64 `json:"confidence"`
|
|
Linked bool `json:"linked"`
|
|
}
|
|
|
|
func newCmdShow(f *cmdutil.Factory) *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "show <gid>",
|
|
Short: "Show a single common tracker pattern by GID",
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
|
|
output := clicmdutil.AddOutputFlag(cmd)
|
|
|
|
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
|
if err := clicmdutil.ValidateOutputFlag(output); err != nil {
|
|
return err
|
|
}
|
|
|
|
id, err := gid.ParseGID(args[0])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid GID %q: %w", args[0], err)
|
|
}
|
|
|
|
pgClient, err := f.PgClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var (
|
|
pattern coredata.CommonTrackerPattern
|
|
thirdPartyName string
|
|
)
|
|
|
|
if err := pgClient.WithConn(
|
|
cmd.Context(),
|
|
func(ctx context.Context, conn pg.Querier) error {
|
|
if err := pattern.LoadByID(ctx, conn, id); err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return fmt.Errorf("no common tracker pattern found for %q", args[0])
|
|
}
|
|
|
|
return fmt.Errorf("cannot load common tracker pattern: %w", err)
|
|
}
|
|
|
|
if pattern.CommonThirdPartyID != nil {
|
|
var party coredata.CommonThirdParty
|
|
if err := party.LoadByID(ctx, conn, *pattern.CommonThirdPartyID); err != nil {
|
|
if !errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return fmt.Errorf("cannot load common third party: %w", err)
|
|
}
|
|
} else {
|
|
thirdPartyName = party.Name
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
if *output == clicmdutil.OutputJSON {
|
|
return clicmdutil.PrintJSON(f.IOStreams.Out, pattern)
|
|
}
|
|
|
|
return renderPatternDetail(f, pattern, thirdPartyName)
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func renderPatternDetail(f *cmdutil.Factory, p coredata.CommonTrackerPattern, thirdPartyName string) error {
|
|
out := f.IOStreams.Out
|
|
label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(20)
|
|
|
|
row := func(name, value string) {
|
|
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render(name), value)
|
|
}
|
|
|
|
row("ID:", p.ID.String())
|
|
row("Tracker type:", string(p.TrackerType))
|
|
row("Match type:", string(p.MatchType))
|
|
row("Pattern:", p.Pattern)
|
|
row("Confidence:", fmt.Sprintf("%.2f", p.Confidence))
|
|
row("Verdict:", string(p.Attribution))
|
|
row("State:", enrichmentState(&p))
|
|
|
|
if p.MaxAgeSeconds != nil {
|
|
row("Max age (s):", fmt.Sprintf("%d", *p.MaxAgeSeconds))
|
|
}
|
|
|
|
if p.CommonThirdPartyID != nil {
|
|
row("Third party:", fmt.Sprintf("%s (%s)", thirdPartyName, p.CommonThirdPartyID.String()))
|
|
} else {
|
|
row("Third party:", "(unlinked)")
|
|
}
|
|
|
|
description := p.Description
|
|
if description == "" {
|
|
description = "(none)"
|
|
}
|
|
|
|
row("Description:", description)
|
|
|
|
row("Enrichment attempts:", fmt.Sprintf("%d", p.EnrichmentAttempts))
|
|
|
|
if p.EnrichmentRequestedAt != nil {
|
|
row("Enrichment queued:", p.EnrichmentRequestedAt.Format("2006-01-02 15:04:05"))
|
|
}
|
|
|
|
if p.LastEnrichmentAttemptAt != nil {
|
|
row("Last attempt:", p.LastEnrichmentAttemptAt.Format("2006-01-02 15:04:05"))
|
|
}
|
|
|
|
row("Created:", p.CreatedAt.Format("2006-01-02 15:04:05"))
|
|
row("Updated:", p.UpdatedAt.Format("2006-01-02 15:04:05"))
|
|
|
|
printPatternEnrichmentDetails(out, label, p)
|
|
|
|
return nil
|
|
}
|
|
|
|
// printPatternEnrichmentDetails renders the run-level status (done,
|
|
// partial, no_result), the agent attribution, and the per-field
|
|
// provenance recorded in the enrichment payload, when present.
|
|
func printPatternEnrichmentDetails(out io.Writer, label lipgloss.Style, p coredata.CommonTrackerPattern) {
|
|
if len(p.Enrichment) == 0 {
|
|
return
|
|
}
|
|
|
|
var meta patternEnrichmentMetadataView
|
|
if err := json.Unmarshal(p.Enrichment, &meta); err != nil {
|
|
return
|
|
}
|
|
|
|
row := func(name, value string) {
|
|
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render(name), value)
|
|
}
|
|
|
|
if meta.Status != "" {
|
|
row("Last run status:", meta.Status)
|
|
}
|
|
|
|
if !meta.AttemptedAt.IsZero() {
|
|
row("Last run recorded:", meta.AttemptedAt.Format("2006-01-02 15:04:05"))
|
|
}
|
|
|
|
if meta.Model != "" {
|
|
row("Enrichment model:", meta.Model)
|
|
}
|
|
|
|
if meta.Error != "" {
|
|
row("Last error:", meta.Error)
|
|
}
|
|
|
|
if meta.Attribution != nil {
|
|
name := meta.Attribution.ThirdPartyName
|
|
if name == "" {
|
|
name = "(none)"
|
|
}
|
|
|
|
linked := "no"
|
|
if meta.Attribution.Linked {
|
|
linked = "yes"
|
|
}
|
|
|
|
row("Agent attribution:", fmt.Sprintf("%s [%s] conf %.2f linked=%s", name, meta.Attribution.Category, meta.Attribution.Confidence, linked))
|
|
}
|
|
|
|
if len(meta.Fields) > 0 {
|
|
names := make([]string, 0, len(meta.Fields))
|
|
for name := range meta.Fields {
|
|
names = append(names, name)
|
|
}
|
|
|
|
sort.Strings(names)
|
|
|
|
_, _ = fmt.Fprintln(out)
|
|
|
|
table := clicmdutil.NewTable("FIELD", "STATUS", "UPDATED")
|
|
|
|
for _, name := range names {
|
|
fm := meta.Fields[name]
|
|
|
|
updated := ""
|
|
if !fm.UpdatedAt.IsZero() {
|
|
updated = fm.UpdatedAt.Format("2006-01-02 15:04:05")
|
|
}
|
|
|
|
table.Row(name, fm.Status, updated)
|
|
}
|
|
|
|
_, _ = fmt.Fprintln(out, table.Render())
|
|
}
|
|
}
|