Skip to content

Secret Scanner

The Secret Scanner detects secrets (API keys, tokens, and other credentials) exposed in a team's Postman workspaces. Reachable via client.SecretScanner().

Requires an add-on

The Secret Scanner API requires a Postman Enterprise plan with the Advanced Security Administration add-on enabled โ€” see Authentication.

Quick Example

import (
    "context"

    postman "github.com/grokify/postman-go"
    "github.com/grokify/postman-go/secretscanner"
)

client, _ := postman.NewClient(postman.WithAPIKey(key))
ctx := context.Background()

// List supported secret types.
types, err := client.SecretScanner().SecretTypes(ctx)

// Query detected secrets.
result, err := client.SecretScanner().Query(ctx, &secretscanner.QueryInput{
    Limit:        50,
    IncludeTotal: true,
})
for _, s := range result.Secrets {
    fmt.Printf("%s in %s: %s\n", s.SecretType, s.WorkspaceID, s.Resolution)
}

Methods

Method Description
Query(ctx, *QueryInput) Query detected secrets, filtered by resolution, secret type, workspace, or resource. Paginated.
UpdateResolution(ctx, secretID, *UpdateResolutionInput) Mark a detected secret as FALSE_POSITIVE, REVOKED, or ACCEPTED_RISK.
Locations(ctx, secretID, *LocationsInput) Find every location (collection, request, environment, ...) where a specific secret was found.
SecretTypes(ctx) List the secret types the scanner supports, including custom team regexes.

Query

result, err := client.SecretScanner().Query(ctx, &secretscanner.QueryInput{
    Resolved:     false,                                      // only unresolved secrets
    Statuses:     []secretscanner.Resolution{secretscanner.ResolutionActive},
    WorkspaceIDs: []string{"workspace-id"},                    // mutually exclusive with Resources
    Limit:        50,
    IncludeTotal: true,
})
// result.Secrets []DetectedSecret, result.NextCursor, result.Total

A nil input returns all results. Resources and WorkspaceIDs are mutually exclusive filters.

When Limit is 0, the SDK sends limit=100, the API's maximum page size. It always sends a limit because, without one, Postman echoes its default page size back as a string, which the response decoder rejects.

To fetch every page, pass NextCursor back as Cursor until it's empty:

in := &secretscanner.QueryInput{}
for {
    page, err := client.SecretScanner().Query(ctx, in)
    if err != nil {
        return err
    }
    for _, s := range page.Secrets {
        fmt.Println(s.SecretID, s.SecretType)
    }
    if page.NextCursor == "" {
        break
    }
    in.Cursor = page.NextCursor
}

UpdateResolution

_, err := client.SecretScanner().UpdateResolution(ctx, "SECRET_ID",
    &secretscanner.UpdateResolutionInput{
        Resolution:  secretscanner.ResolutionFalsePositive,
        WorkspaceID: "WORKSPACE_ID",
    })

Resolution accepts ResolutionFalsePositive, ResolutionRevoked, or ResolutionAcceptedRisk โ€” ResolutionActive cannot be set directly.

Locations

locations, err := client.SecretScanner().Locations(ctx, "SECRET_ID",
    &secretscanner.LocationsInput{WorkspaceID: "WORKSPACE_ID"})
for _, loc := range locations.Locations {
    fmt.Println(loc.Location, loc.URL, loc.ResourceType)
}
// The secret's resolution history.
for _, e := range locations.ActivityFeed {
    fmt.Println(e.Status, e.ResolvedAt, e.ResolvedBy)
}

Limit defaults to 100, as with Query. For a secret that is still active, the activity-feed entry has no ResolvedAt or ResolvedBy. Those fields are then empty ("" and 0), and Cursor is empty when there's no cursor.

SecretTypes

types, err := client.SecretScanner().SecretTypes(ctx)
for _, t := range types.Types {
    fmt.Println(t.ID, t.Name, t.Origin) // Origin: DEFAULT or TEAM_REGEX
}

Exporting to OCSF

To send findings to a SIEM or data lake, convert them to OCSF Detection Finding events with the ocsf package โ€” see Exporting Findings to OCSF.

Reference

Source: secretscanner/ ยท GoDoc