Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions binary/proto/scan_result.proto
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ message SecretData {
OnePasswordRecoveryCode onepassword_recovery_code = 41;
OnePasswordConnectToken onepassword_connect_token = 42;
Pgpass pgpass = 43;
NpmJSAccessToken npmjs_access_token = 44;
}

message GCPSAK {
Expand Down Expand Up @@ -805,6 +806,10 @@ message SecretData {
string key = 1;
}

message NpmJSAccessToken {
string Token = 1;
}

message GithubAppRefreshToken {
string token = 1;
}
Expand Down
346 changes: 206 additions & 140 deletions binary/proto/scan_result_go_proto/scan_result.pb.go

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions binary/proto/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
veleshashicorpvault "github.com/google/osv-scalibr/veles/secrets/hashicorpvault"
veleshashicorpcloudplatform "github.com/google/osv-scalibr/veles/secrets/hcp"
"github.com/google/osv-scalibr/veles/secrets/huggingfaceapikey"
"github.com/google/osv-scalibr/veles/secrets/npmjsaccesstoken"
velesonepasswordkeys "github.com/google/osv-scalibr/veles/secrets/onepasswordkeys"
velesopenai "github.com/google/osv-scalibr/veles/secrets/openai"
velesperplexity "github.com/google/osv-scalibr/veles/secrets/perplexityapikey"
Expand Down Expand Up @@ -115,6 +116,8 @@ func velesSecretToProto(s veles.Secret) (*spb.SecretData, error) {
return dockerHubPATToProto(t), nil
case velesdigitalocean.DigitaloceanAPIToken:
return digitaloceanAPIKeyToProto(t), nil
case npmjsaccesstoken.NpmJSAccessToken:
return npmJSAccessTokenToProto(t), nil
case velesslacktoken.SlackAppConfigAccessToken:
return slackAppConfigAccessTokenToProto(t), nil
case velesslacktoken.SlackAppConfigRefreshToken:
Expand Down Expand Up @@ -217,6 +220,16 @@ func digitaloceanAPIKeyToProto(s velesdigitalocean.DigitaloceanAPIToken) *spb.Se
}
}

func npmJSAccessTokenToProto(s npmjsaccesstoken.NpmJSAccessToken) *spb.SecretData {
return &spb.SecretData{
Secret: &spb.SecretData_NpmjsAccessToken{
NpmjsAccessToken: &spb.SecretData_NpmJSAccessToken{
Token: s.Token,
},
},
}
}

func slackAppLevelTokenToProto(s velesslacktoken.SlackAppLevelToken) *spb.SecretData {
return &spb.SecretData{
Secret: &spb.SecretData_SlackAppLevelToken_{
Expand Down Expand Up @@ -717,6 +730,8 @@ func velesSecretToStruct(s *spb.SecretData) (veles.Secret, error) {
return gitlabPATToStruct(s.GetGitlabPat()), nil
case *spb.SecretData_Digitalocean:
return digitalOceanAPITokenToStruct(s.GetDigitalocean()), nil
case *spb.SecretData_NpmjsAccessToken:
return npmJSAccessTokenToStruct(s.GetNpmjsAccessToken()), nil
case *spb.SecretData_SlackAppConfigRefreshToken_:
return slackAppConfigRefreshTokenToStruct(s.GetSlackAppConfigRefreshToken()), nil
case *spb.SecretData_SlackAppConfigAccessToken_:
Expand Down Expand Up @@ -834,6 +849,12 @@ func digitalOceanAPITokenToStruct(kPB *spb.SecretData_DigitalOceanAPIToken) vele
}
}

func npmJSAccessTokenToStruct(kPB *spb.SecretData_NpmJSAccessToken) npmjsaccesstoken.NpmJSAccessToken {
return npmjsaccesstoken.NpmJSAccessToken{
Token: kPB.GetToken(),
}
}

func slackAppLevelTokenToStruct(kPB *spb.SecretData_SlackAppLevelToken) velesslacktoken.SlackAppLevelToken {
return velesslacktoken.SlackAppLevelToken{
Token: kPB.GetToken(),
Expand Down
5 changes: 3 additions & 2 deletions docs/supported_inventory_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ See the docs on [how to add a new Extractor](/docs/new_extractor.md).

### Secrets

| Type | Extractor Plugin |
|-----------------------------------|--------------------------------------|
| Type | Extractor Plugin |
|---------------------------------------------|--------------------------------------|
| Anthropic API key | `secrets/anthropicapikey` |
| Azure Token | `secrets/azuretoken` |
| DigitalOcean API key | `secrets/digitaloceanapikey` |
Expand All @@ -131,6 +131,7 @@ See the docs on [how to add a new Extractor](/docs/new_extractor.md).
| 1Password Secret Key | `secrets/onepasswordsecretkey` |
| 1Password Service Token | `secrets/onepasswordservicetoken` |
| 1Password Recovery Code | `secrets/onepasswordrecoverycode` |
| npmjs Registry Access Tokens | `secrets/npmjsaccesstoken` |
| OpenAI API key | `secrets/openai` |
| Perplexity API key | `secrets/perplexityapikey` |
| Postgres pgpass file | `secrets/pgpass` |
Expand Down
2 changes: 2 additions & 0 deletions enricher/enricherlist/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
"github.com/google/osv-scalibr/veles/secrets/hashicorpvault"
"github.com/google/osv-scalibr/veles/secrets/hcp"
"github.com/google/osv-scalibr/veles/secrets/huggingfaceapikey"
"github.com/google/osv-scalibr/veles/secrets/npmjsaccesstoken"
"github.com/google/osv-scalibr/veles/secrets/openai"
"github.com/google/osv-scalibr/veles/secrets/perplexityapikey"
"github.com/google/osv-scalibr/veles/secrets/postmanapikey"
Expand Down Expand Up @@ -83,6 +84,7 @@ var (
fromVeles(anthropicapikey.NewWorkspaceValidator(), "secrets/anthropicapikeyworkspacevalidate", 0),
fromVeles(anthropicapikey.NewModelValidator(), "secrets/anthropicapikeymodelvalidate", 0),
fromVeles(digitaloceanapikey.NewValidator(), "secrets/digitaloceanapikeyvalidate", 0),
fromVeles(npmjsaccesstoken.NewValidator(), "secrets/npmjsaccesstoken", 0),
fromVeles(slacktoken.NewAppLevelTokenValidator(), "secrets/slackappleveltokenvalidate", 0),
fromVeles(slacktoken.NewAppConfigRefreshTokenValidator(), "secrets/slackconfigrefreshtokenvalidate", 0),
fromVeles(slacktoken.NewAppConfigAccessTokenValidator(), "secrets/slackconfigaccesstokenvalidate", 0),
Expand Down
2 changes: 2 additions & 0 deletions extractor/filesystem/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ import (
"github.com/google/osv-scalibr/veles/secrets/hashicorpvault"
"github.com/google/osv-scalibr/veles/secrets/hcp"
"github.com/google/osv-scalibr/veles/secrets/huggingfaceapikey"
"github.com/google/osv-scalibr/veles/secrets/npmjsaccesstoken"
"github.com/google/osv-scalibr/veles/secrets/onepasswordkeys"
"github.com/google/osv-scalibr/veles/secrets/openai"
"github.com/google/osv-scalibr/veles/secrets/perplexityapikey"
Expand Down Expand Up @@ -274,6 +275,7 @@ var (
{azuretoken.NewDetector(), "secrets/azuretoken", 0},
{azurestorageaccountaccesskey.NewDetector(), "secrets/azurestorageaccountaccesskey", 0},
{digitaloceanapikey.NewDetector(), "secrets/digitaloceanapikey", 0},
{npmjsaccesstoken.NewDetector(), "secrets/npmjsaccesstoken", 0},
{slacktoken.NewAppConfigAccessTokenDetector(), "secrets/slackappconfigaccesstoken", 0},
{slacktoken.NewAppConfigRefreshTokenDetector(), "secrets/slackappconfigrefreshtoken", 0},
{slacktoken.NewAppLevelTokenDetector(), "secrets/slackappleveltoken", 0},
Expand Down
44 changes: 44 additions & 0 deletions veles/secrets/npmjsaccesstoken/detector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package npmjsaccesstoken contains a Veles Secret type and a Detector for
// npm.js Access Tokens (prefix `npm_`).
package npmjsaccesstoken

import (
"regexp"

"github.com/google/osv-scalibr/veles"
"github.com/google/osv-scalibr/veles/secrets/common/simpletoken"
)

// maxTokenLength is the maximum size of an npm.js access token.
const maxTokenLength = 40

// tokenRe is a regular expression that matches an npm.js access token.
// npm.js access tokens have the form: `npm_` followed by 36
// alphanumeric characters.
var tokenRe = regexp.MustCompile(`npm_[a-zA-Z0-9]{36}`)

// NewDetector returns a new simpletoken.Detector that matches
// npm.js access tokens.
func NewDetector() veles.Detector {
return simpletoken.Detector{
MaxLen: maxTokenLength,
Re: tokenRe,
FromMatch: func(b []byte) (veles.Secret, bool) {
return NpmJSAccessToken{Token: string(b)}, true
},
}
}
142 changes: 142 additions & 0 deletions veles/secrets/npmjsaccesstoken/detector_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package npmjsaccesstoken_test

import (
"fmt"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/osv-scalibr/veles"
"github.com/google/osv-scalibr/veles/secrets/npmjsaccesstoken"
)

const testKey = `npm_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8`

// TestDetector_truePositives tests for cases where we know the Detector
// will find a npm.js access token/s.
func TestDetector_truePositives(t *testing.T) {
engine, err := veles.NewDetectionEngine([]veles.Detector{npmjsaccesstoken.NewDetector()})
if err != nil {
t.Fatal(err)
}
cases := []struct {
name string
input string
want []veles.Secret
}{{
name: "simple matching string",
input: testKey,
want: []veles.Secret{
npmjsaccesstoken.NpmJSAccessToken{Token: testKey},
},
}, {
name: "match at end of string",
input: `NPM_TOKEN=` + testKey,
want: []veles.Secret{
npmjsaccesstoken.NpmJSAccessToken{Token: testKey},
},
}, {
name: "match in middle of string",
input: `NPM_TOKEN="` + testKey + `"`,
want: []veles.Secret{
npmjsaccesstoken.NpmJSAccessToken{Token: testKey},
},
}, {
name: "multiple matches",
input: testKey + testKey + testKey,
want: []veles.Secret{
npmjsaccesstoken.NpmJSAccessToken{Token: testKey},
npmjsaccesstoken.NpmJSAccessToken{Token: testKey},
npmjsaccesstoken.NpmJSAccessToken{Token: testKey},
},
}, {
name: "multiple distinct matches",
input: testKey + "\n" + testKey[:len(testKey)-1] + "a",
want: []veles.Secret{
npmjsaccesstoken.NpmJSAccessToken{Token: testKey},
npmjsaccesstoken.NpmJSAccessToken{Token: testKey[:len(testKey)-1] + "a"},
},
}, {
name: "larger input containing key",
input: fmt.Sprintf(`
:test_npm_token: npm-test
:NPM_TOKEN: %s
`, testKey),
want: []veles.Secret{
npmjsaccesstoken.NpmJSAccessToken{Token: testKey},
},
}, {
name: "potential match longer than max key length",
input: testKey + `extra`,
want: []veles.Secret{
npmjsaccesstoken.NpmJSAccessToken{Token: testKey},
},
}}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := engine.Detect(t.Context(), strings.NewReader(tc.input))
if err != nil {
t.Errorf("Detect() error: %v, want nil", err)
}
fmt.Printf("got = %+v\n", got)
if diff := cmp.Diff(tc.want, got, cmpopts.EquateEmpty()); diff != "" {
t.Errorf("Detect() diff (-want +got):\n%s", diff)
}
})
}
}

// TestDetector_trueNegatives tests for cases where we know the Detector
// will not find an npm.js access token.
func TestDetector_trueNegatives(t *testing.T) {
engine, err := veles.NewDetectionEngine([]veles.Detector{npmjsaccesstoken.NewDetector()})
if err != nil {
t.Fatal(err)
}
cases := []struct {
name string
input string
want []veles.Secret
}{{
name: "empty input",
input: "",
}, {
name: "short key should not match",
input: testKey[:len(testKey)-1],
}, {
name: "invalid character in key should not match",
input: `npm_!@#$%^&*()_+{}[]|:;<>?,./~` + `123456`,
}, {
name: "incorrect prefix should not match",
input: `npp_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6`,
}, {
name: "prefix missing should not match",
input: `a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8`,
}}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := engine.Detect(t.Context(), strings.NewReader(tc.input))
if err != nil {
t.Errorf("Detect() error: %v, want nil", err)
}
if diff := cmp.Diff(tc.want, got, cmpopts.EquateEmpty()); diff != "" {
t.Errorf("Detect() diff (-want +got):\n%s", diff)
}
})
}
}
22 changes: 22 additions & 0 deletions veles/secrets/npmjsaccesstoken/npmjsaccesstoken.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package npmjsaccesstoken

// NpmJSAccessToken is a Veles Secret that holds relevant information for a
// DigitalOcean API key (prefix `dop_v1_`).
// DigitaloceanAPIToken represents an API key used to authenticate requests
type NpmJSAccessToken struct {
Token string
}
Loading