Skip to content

Commit 25691ea

Browse files
Add Clojars deploy token detector
1 parent 0f5758f commit 25691ea

4 files changed

Lines changed: 204 additions & 0 deletions

File tree

extractor/filesystem/list/list.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ import (
131131
"github.com/google/osv-scalibr/veles/secrets/azurestorageaccountaccesskey"
132132
"github.com/google/osv-scalibr/veles/secrets/azuretoken"
133133
"github.com/google/osv-scalibr/veles/secrets/circleci"
134+
"github.com/google/osv-scalibr/veles/secrets/clojars"
134135
"github.com/google/osv-scalibr/veles/secrets/cratesioapitoken"
135136
"github.com/google/osv-scalibr/veles/secrets/cursorapikey"
136137
"github.com/google/osv-scalibr/veles/secrets/denopat"
@@ -372,6 +373,7 @@ var (
372373
{azurestorageaccountaccesskey.NewDetector(), "secrets/azurestorageaccountaccesskey", 0},
373374
{circleci.NewPersonalAccessTokenDetector(), "secrets/circlecipat", 0},
374375
{circleci.NewProjectTokenDetector(), "secrets/circleciproject", 0},
376+
{clojars.NewDetector(), "secrets/clojars", 0},
375377
{cursorapikey.NewDetector(), "secrets/cursorapikey", 0},
376378
{digitaloceanapikey.NewDetector(), "secrets/digitaloceanapikey", 0},
377379
{pypiapitoken.NewDetector(), "secrets/pypiapitoken", 0},

veles/secrets/clojars/clojars.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package clojars
16+
17+
// DeployToken is a Veles Secret that holds relevant information for a
18+
// Clojars Deploy Token (prefix `CLOJARS_`).
19+
type DeployToken struct {
20+
Token string
21+
}

veles/secrets/clojars/detector.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package clojars contains a Veles Secret type and Detector for Clojars Deploy
16+
// Tokens (prefix `CLOJARS_`).
17+
package clojars
18+
19+
import (
20+
"regexp"
21+
22+
"github.com/google/osv-scalibr/veles"
23+
"github.com/google/osv-scalibr/veles/secrets/common/simpletoken"
24+
)
25+
26+
// maxTokenLength is the maximum size of a Clojars Deploy Token.
27+
const maxTokenLength = 68
28+
29+
// deployTokenRe is a regular expression that matches a Clojars Deploy Token.
30+
// Deploy tokens have the form: `CLOJARS_` followed by exactly 60 hexadecimal
31+
// characters.
32+
var deployTokenRe = regexp.MustCompile(`\bCLOJARS_[A-Fa-f0-9]{60}\b`)
33+
34+
// NewDetector returns a new simpletoken.Detector that matches Clojars Deploy
35+
// Tokens.
36+
func NewDetector() veles.Detector {
37+
return simpletoken.Detector{
38+
MaxLen: maxTokenLength,
39+
Re: deployTokenRe,
40+
FromMatch: func(b []byte) (veles.Secret, bool) {
41+
return DeployToken{Token: string(b)}, true
42+
},
43+
}
44+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package clojars_test
16+
17+
import (
18+
"fmt"
19+
"strings"
20+
"testing"
21+
22+
"github.com/google/go-cmp/cmp"
23+
"github.com/google/go-cmp/cmp/cmpopts"
24+
"github.com/google/osv-scalibr/veles"
25+
"github.com/google/osv-scalibr/veles/secrets/clojars"
26+
"github.com/google/osv-scalibr/veles/velestest"
27+
)
28+
29+
const testDeployToken = `CLOJARS_0123456789abcdefABCDEF0123456789abcdefABCDEF0123456789abcdef`
30+
31+
func TestDetectorAcceptance(t *testing.T) {
32+
velestest.AcceptDetector(
33+
t,
34+
clojars.NewDetector(),
35+
testDeployToken,
36+
clojars.DeployToken{Token: testDeployToken},
37+
)
38+
}
39+
40+
func TestDetector_truePositives(t *testing.T) {
41+
engine, err := veles.NewDetectionEngine([]veles.Detector{clojars.NewDetector()})
42+
if err != nil {
43+
t.Fatal(err)
44+
}
45+
otherToken := testDeployToken[:len(testDeployToken)-1] + "0"
46+
cases := []struct {
47+
name string
48+
input string
49+
want []veles.Secret
50+
}{{
51+
name: "simple matching string",
52+
input: testDeployToken,
53+
want: []veles.Secret{
54+
clojars.DeployToken{Token: testDeployToken},
55+
},
56+
}, {
57+
name: "match at end of env assignment",
58+
input: `CLOJARS_DEPLOY_TOKEN=` + testDeployToken,
59+
want: []veles.Secret{
60+
clojars.DeployToken{Token: testDeployToken},
61+
},
62+
}, {
63+
name: "match in quoted config value",
64+
input: `:deploy-token "` + testDeployToken + `"`,
65+
want: []veles.Secret{
66+
clojars.DeployToken{Token: testDeployToken},
67+
},
68+
}, {
69+
name: "multiple distinct matches",
70+
input: testDeployToken + "\n" + otherToken,
71+
want: []veles.Secret{
72+
clojars.DeployToken{Token: testDeployToken},
73+
clojars.DeployToken{Token: otherToken},
74+
},
75+
}, {
76+
name: "larger input containing token",
77+
input: fmt.Sprintf(`
78+
:username "example"
79+
:password "%s"
80+
`, testDeployToken),
81+
want: []veles.Secret{
82+
clojars.DeployToken{Token: testDeployToken},
83+
},
84+
}}
85+
for _, tc := range cases {
86+
t.Run(tc.name, func(t *testing.T) {
87+
got, err := engine.Detect(t.Context(), strings.NewReader(tc.input))
88+
if err != nil {
89+
t.Errorf("Detect() error: %v, want nil", err)
90+
}
91+
if diff := cmp.Diff(tc.want, got, cmpopts.EquateEmpty()); diff != "" {
92+
t.Errorf("Detect() diff (-want +got):\n%s", diff)
93+
}
94+
})
95+
}
96+
}
97+
98+
func TestDetector_trueNegatives(t *testing.T) {
99+
engine, err := veles.NewDetectionEngine([]veles.Detector{clojars.NewDetector()})
100+
if err != nil {
101+
t.Fatal(err)
102+
}
103+
cases := []struct {
104+
name string
105+
input string
106+
want []veles.Secret
107+
}{{
108+
name: "empty input",
109+
input: "",
110+
}, {
111+
name: "short token should not match",
112+
input: testDeployToken[:len(testDeployToken)-1],
113+
}, {
114+
name: "long token should not match",
115+
input: testDeployToken + "0",
116+
}, {
117+
name: "invalid hex character should not match",
118+
input: testDeployToken[:len(testDeployToken)-1] + "g",
119+
}, {
120+
name: "incorrect prefix should not match",
121+
input: "CLOJURE_" + testDeployToken[len("CLOJARS_"):],
122+
}, {
123+
name: "lowercase prefix should not match",
124+
input: strings.ToLower(testDeployToken[:len("CLOJARS_")]) + testDeployToken[len("CLOJARS_"):],
125+
}}
126+
for _, tc := range cases {
127+
t.Run(tc.name, func(t *testing.T) {
128+
got, err := engine.Detect(t.Context(), strings.NewReader(tc.input))
129+
if err != nil {
130+
t.Errorf("Detect() error: %v, want nil", err)
131+
}
132+
if diff := cmp.Diff(tc.want, got, cmpopts.EquateEmpty()); diff != "" {
133+
t.Errorf("Detect() diff (-want +got):\n%s", diff)
134+
}
135+
})
136+
}
137+
}

0 commit comments

Comments
 (0)