diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 8d6c9f084..c94243815 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -48,6 +48,7 @@ import ( "go.probo.inc/probo/pkg/cmd/task" "go.probo.inc/probo/pkg/cmd/tia" trackerpattern "go.probo.inc/probo/pkg/cmd/tracker-pattern" + trackerresource "go.probo.inc/probo/pkg/cmd/tracker-resource" trustcenter "go.probo.inc/probo/pkg/cmd/trust-center" "go.probo.inc/probo/pkg/cmd/user" "go.probo.inc/probo/pkg/cmd/vendormgmt" @@ -100,6 +101,7 @@ func NewCmdRoot(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(cookiebanner.NewCmdCookieBanner(f)) cmd.AddCommand(cookiecategory.NewCmdCookieCategory(f)) cmd.AddCommand(trackerpattern.NewCmdTrackerPattern(f)) + cmd.AddCommand(trackerresource.NewCmdTrackerResource(f)) cmd.AddCommand(datum.NewCmdDatum(f)) cmd.AddCommand(document.NewCmdDocument(f)) cmd.AddCommand(dpia.NewCmdDPIA(f)) diff --git a/pkg/cmd/tracker-resource/create/create.go b/pkg/cmd/tracker-resource/create/create.go new file mode 100644 index 000000000..f316a84df --- /dev/null +++ b/pkg/cmd/tracker-resource/create/create.go @@ -0,0 +1,165 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package create + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const createMutation = ` +mutation($input: CreateTrackerResourceInput!) { + createTrackerResource(input: $input) { + trackerResourceEdge { + node { + id + displayName + } + } + cookieBanner { + id + } + } +} +` + +type createResponse struct { + CreateTrackerResource struct { + TrackerResourceEdge struct { + Node struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + } `json:"node"` + } `json:"trackerResourceEdge"` + } `json:"createTrackerResource"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagCategoryID string + flagResourceType string + flagOrigin string + flagPath string + flagDisplayName string + flagDescription string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new tracker resource", + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if f.IOStreams.IsInteractive() { + if flagResourceType == "" { + if err := huh.NewSelect[string](). + Title("Resource type"). + Options( + huh.NewOption("Script", "SCRIPT"), + huh.NewOption("Iframe", "IFRAME"), + ). + Value(&flagResourceType).Run(); err != nil { + return err + } + } + if flagOrigin == "" { + if err := huh.NewInput().Title("Origin").Value(&flagOrigin).Run(); err != nil { + return err + } + } + if flagPath == "" { + if err := huh.NewInput().Title("Path").Value(&flagPath).Run(); err != nil { + return err + } + } + if flagDisplayName == "" { + if err := huh.NewInput().Title("Display name").Value(&flagDisplayName).Run(); err != nil { + return err + } + } + } + + if flagResourceType == "" { + return fmt.Errorf("resource-type is required; pass --resource-type or run interactively") + } + if flagOrigin == "" { + return fmt.Errorf("origin is required; pass --origin or run interactively") + } + if flagPath == "" { + return fmt.Errorf("path is required; pass --path or run interactively") + } + if flagDisplayName == "" { + return fmt.Errorf("display-name is required; pass --display-name or run interactively") + } + + input := map[string]any{ + "cookieCategoryId": flagCategoryID, + "type": flagResourceType, + "origin": flagOrigin, + "path": flagPath, + "displayName": flagDisplayName, + } + if flagDescription != "" { + input["description"] = flagDescription + } + + data, err := client.Do(createMutation, map[string]any{"input": input}) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.CreateTrackerResource.TrackerResourceEdge.Node + _, _ = fmt.Fprintf(f.IOStreams.Out, "Created tracker resource %s (%s)\n", r.ID, r.DisplayName) + + return nil + }, + } + + cmd.Flags().StringVar(&flagCategoryID, "category-id", "", "Cookie category ID (required)") + _ = cmd.MarkFlagRequired("category-id") + cmd.Flags().StringVar(&flagResourceType, "resource-type", "", "Resource type: SCRIPT or IFRAME (required)") + cmd.Flags().StringVar(&flagOrigin, "origin", "", "Origin URL (required)") + cmd.Flags().StringVar(&flagPath, "path", "", "Resource path (required)") + cmd.Flags().StringVar(&flagDisplayName, "display-name", "", "Display name (required)") + cmd.Flags().StringVar(&flagDescription, "description", "", "Description") + + return cmd +} diff --git a/pkg/cmd/tracker-resource/delete/delete.go b/pkg/cmd/tracker-resource/delete/delete.go new file mode 100644 index 000000000..aec453d17 --- /dev/null +++ b/pkg/cmd/tracker-resource/delete/delete.go @@ -0,0 +1,92 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package delete + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const deleteMutation = ` +mutation($input: DeleteTrackerResourceInput!) { + deleteTrackerResource(input: $input) { + deletedTrackerResourceId + cookieBanner { + id + } + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a tracker resource", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete tracker resource: confirmation required, use --yes to confirm") + } + var confirmed bool + if err := huh.NewConfirm().Title(fmt.Sprintf("Delete tracker resource %s?", args[0])).Value(&confirmed).Run(); err != nil { + return err + } + if !confirmed { + return nil + } + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + _, err = client.Do(deleteMutation, map[string]any{ + "input": map[string]any{"trackerResourceId": args[0]}, + }) + if err != nil { + return err + } + + _, _ = fmt.Fprintf(f.IOStreams.Out, "Deleted tracker resource %s\n", args[0]) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/tracker-resource/list/list.go b/pkg/cmd/tracker-resource/list/list.go new file mode 100644 index 000000000..e84f09c78 --- /dev/null +++ b/pkg/cmd/tracker-resource/list/list.go @@ -0,0 +1,168 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package list + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const listQuery = ` +query($id: ID!, $first: Int, $after: CursorKey) { + node(id: $id) { + __typename + ... on CookieCategory { + trackerResources(first: $first, after: $after) { + totalCount + edges { + node { + id + type + origin + path + displayName + excluded + lastDetectedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type trackerResource struct { + ID string `json:"id"` + Type string `json:"type"` + Origin string `json:"origin"` + Path string `json:"path"` + DisplayName string `json:"displayName"` + Excluded bool `json:"excluded"` + LastDetectedAt *string `json:"lastDetectedAt"` +} + +func NewCmdList(f *cmdutil.Factory) *cobra.Command { + var ( + flagCategoryID string + flagLimit int + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List tracker resources in a category", + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + variables := map[string]any{"id": flagCategoryID} + + resources, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[trackerResource], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + TrackerResources api.Connection[trackerResource] `json:"trackerResources"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + if resp.Node == nil { + return nil, fmt.Errorf("cookie category %s not found", flagCategoryID) + } + if resp.Node.Typename != "CookieCategory" { + return nil, fmt.Errorf("expected CookieCategory node, got %s", resp.Node.Typename) + } + return &resp.Node.TrackerResources, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resources) + } + + if len(resources) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No tracker resources found.") + return nil + } + + rows := make([][]string, 0, len(resources)) + for _, r := range resources { + excluded := "" + if r.Excluded { + excluded = "yes" + } + lastDetected := "" + if r.LastDetectedAt != nil { + lastDetected = cmdutil.FormatTime(*r.LastDetectedAt) + } + rows = append(rows, []string{r.ID, r.Type, r.Origin, r.Path, r.DisplayName, excluded, lastDetected}) + } + + t := cmdutil.NewTable("ID", "TYPE", "ORIGIN", "PATH", "DISPLAY NAME", "EXCLUDED", "LAST DETECTED").Rows(rows...) + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(resources) { + _, _ = fmt.Fprintf(f.IOStreams.ErrOut, "\nShowing %d of %d tracker resources\n", len(resources), totalCount) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagCategoryID, "category-id", "", "Cookie category ID (required)") + _ = cmd.MarkFlagRequired("category-id") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of items") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/tracker-resource/move/move.go b/pkg/cmd/tracker-resource/move/move.go new file mode 100644 index 000000000..985b2f285 --- /dev/null +++ b/pkg/cmd/tracker-resource/move/move.go @@ -0,0 +1,107 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package move + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const moveMutation = ` +mutation($input: MoveTrackerResourceToCategoryInput!) { + moveTrackerResourceToCategory(input: $input) { + trackerResource { + id + cookieCategory { + id + name + } + } + cookieBanner { + id + } + } +} +` + +type moveResponse struct { + MoveTrackerResourceToCategory struct { + TrackerResource struct { + ID string `json:"id"` + CookieCategory struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"cookieCategory"` + } `json:"trackerResource"` + } `json:"moveTrackerResourceToCategory"` +} + +func NewCmdMove(f *cmdutil.Factory) *cobra.Command { + var flagTargetCategoryID string + + cmd := &cobra.Command{ + Use: "move ", + Short: "Move a tracker resource to a different category", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + data, err := client.Do(moveMutation, map[string]any{ + "input": map[string]any{ + "trackerResourceId": args[0], + "targetCookieCategoryId": flagTargetCategoryID, + }, + }) + if err != nil { + return err + } + + var resp moveResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.MoveTrackerResourceToCategory.TrackerResource + _, _ = fmt.Fprintf(f.IOStreams.Out, "Moved tracker resource %s to category %s\n", r.ID, r.CookieCategory.Name) + + return nil + }, + } + + cmd.Flags().StringVar(&flagTargetCategoryID, "target-category-id", "", "Target cookie category ID (required)") + _ = cmd.MarkFlagRequired("target-category-id") + + return cmd +} diff --git a/pkg/cmd/tracker-resource/tracker_resource.go b/pkg/cmd/tracker-resource/tracker_resource.go new file mode 100644 index 000000000..75a05e9fc --- /dev/null +++ b/pkg/cmd/tracker-resource/tracker_resource.go @@ -0,0 +1,42 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package trackerresource + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/tracker-resource/create" + "go.probo.inc/probo/pkg/cmd/tracker-resource/delete" + "go.probo.inc/probo/pkg/cmd/tracker-resource/list" + "go.probo.inc/probo/pkg/cmd/tracker-resource/move" + "go.probo.inc/probo/pkg/cmd/tracker-resource/update" + "go.probo.inc/probo/pkg/cmd/tracker-resource/view" +) + +func NewCmdTrackerResource(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "tracker-resource ", + Short: "Manage tracker resources", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + cmd.AddCommand(move.NewCmdMove(f)) + + return cmd +} diff --git a/pkg/cmd/tracker-resource/update/update.go b/pkg/cmd/tracker-resource/update/update.go new file mode 100644 index 000000000..ea9bce0ad --- /dev/null +++ b/pkg/cmd/tracker-resource/update/update.go @@ -0,0 +1,117 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package update + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const updateMutation = ` +mutation($input: UpdateTrackerResourceInput!) { + updateTrackerResource(input: $input) { + trackerResource { + id + displayName + } + cookieBanner { + id + } + } +} +` + +type updateResponse struct { + UpdateTrackerResource struct { + TrackerResource struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + } `json:"trackerResource"` + } `json:"updateTrackerResource"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagDisplayName string + flagDescription string + flagExcluded bool + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a tracker resource", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + input := map[string]any{"trackerResourceId": args[0]} + + if cmd.Flags().Changed("display-name") { + input["displayName"] = flagDisplayName + } + if cmd.Flags().Changed("description") { + input["description"] = flagDescription + } + if cmd.Flags().Changed("excluded") { + input["excluded"] = flagExcluded + } + + if len(input) == 1 { + return fmt.Errorf("at least one field must be specified for update") + } + + data, err := client.Do(updateMutation, map[string]any{"input": input}) + if err != nil { + return err + } + + var resp updateResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.UpdateTrackerResource.TrackerResource + _, _ = fmt.Fprintf(f.IOStreams.Out, "Updated tracker resource %s (%s)\n", r.ID, r.DisplayName) + + return nil + }, + } + + cmd.Flags().StringVar(&flagDisplayName, "display-name", "", "Display name") + cmd.Flags().StringVar(&flagDescription, "description", "", "Description") + cmd.Flags().BoolVar(&flagExcluded, "excluded", false, "Exclude resource from consent banner") + + return cmd +} diff --git a/pkg/cmd/tracker-resource/view/view.go b/pkg/cmd/tracker-resource/view/view.go new file mode 100644 index 000000000..b4a0a27aa --- /dev/null +++ b/pkg/cmd/tracker-resource/view/view.go @@ -0,0 +1,140 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on TrackerResource { + id + type + origin + path + displayName + description + excluded + lastDetectedAt + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Type string `json:"type"` + Origin string `json:"origin"` + Path string `json:"path"` + DisplayName string `json:"displayName"` + Description string `json:"description"` + Excluded bool `json:"excluded"` + LastDetectedAt *string `json:"lastDetectedAt"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a tracker resource", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + data, err := client.Do(viewQuery, map[string]any{"id": args[0]}) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil || resp.Node.Typename != "TrackerResource" { + return fmt.Errorf("tracker resource %s not found", args[0]) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + v := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(v.DisplayName)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), v.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Type:"), v.Type) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Origin:"), v.Origin) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Path:"), v.Path) + _, _ = fmt.Fprintf(out, "%s%v\n", label.Render("Excluded:"), v.Excluded) + if v.Description != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), v.Description) + } + if v.LastDetectedAt != nil { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Last Detected:"), cmdutil.FormatTime(*v.LastDetectedAt)) + } + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(v.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(v.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +}