Skip to content

Commit 55c87fa

Browse files
Add Databricks PAT detector
1 parent 25691ea commit 55c87fa

4 files changed

Lines changed: 216 additions & 0 deletions

File tree

extractor/filesystem/list/list.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ import (
134134
"github.com/google/osv-scalibr/veles/secrets/clojars"
135135
"github.com/google/osv-scalibr/veles/secrets/cratesioapitoken"
136136
"github.com/google/osv-scalibr/veles/secrets/cursorapikey"
137+
"github.com/google/osv-scalibr/veles/secrets/databrickspat"
137138
"github.com/google/osv-scalibr/veles/secrets/denopat"
138139
"github.com/google/osv-scalibr/veles/secrets/digitaloceanapikey"
139140
"github.com/google/osv-scalibr/veles/secrets/discordbottoken"
@@ -375,6 +376,7 @@ var (
375376
{circleci.NewProjectTokenDetector(), "secrets/circleciproject", 0},
376377
{clojars.NewDetector(), "secrets/clojars", 0},
377378
{cursorapikey.NewDetector(), "secrets/cursorapikey", 0},
379+
{databrickspat.NewDetector(), "secrets/databrickspat", 0},
378380
{digitaloceanapikey.NewDetector(), "secrets/digitaloceanapikey", 0},
379381
{pypiapitoken.NewDetector(), "secrets/pypiapitoken", 0},
380382
{cratesioapitoken.NewDetector(), "secrets/cratesioapitoken", 0},
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 databrickspat
16+
17+
// UserAccountPAT is a Veles Secret that holds relevant information for a
18+
// Databricks User Account Personal Access Token (prefix `dapi`).
19+
type UserAccountPAT struct {
20+
Token string
21+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 databrickspat contains a Veles Secret type and Detector for
16+
// Databricks User Account Personal Access Tokens (prefix `dapi`).
17+
package databrickspat
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 Databricks User Account PAT, plus
27+
// one optional terminating delimiter consumed by patRe.
28+
const maxTokenLength = 40
29+
30+
// patRe is a regular expression that matches a Databricks User Account PAT.
31+
// Databricks API tokens have the form: `dapi` followed by 32 lowercase
32+
// hexadecimal characters, with an optional single digit suffix.
33+
var patRe = regexp.MustCompile(`\bdapi[a-f0-9]{32}(?:-\d)?(?:[^A-Za-z0-9-]|$)`)
34+
35+
// NewDetector returns a new simpletoken.Detector that matches Databricks User
36+
// Account Personal Access Tokens.
37+
func NewDetector() veles.Detector {
38+
return simpletoken.Detector{
39+
MaxLen: maxTokenLength,
40+
Re: patRe,
41+
FromMatch: func(b []byte) (veles.Secret, bool) {
42+
if len(b) > 0 && !isHexChar(b[len(b)-1]) {
43+
b = b[:len(b)-1]
44+
}
45+
return UserAccountPAT{Token: string(b)}, true
46+
},
47+
}
48+
}
49+
50+
func isHexChar(b byte) bool {
51+
return (b >= 'a' && b <= 'f') || (b >= '0' && b <= '9')
52+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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 databrickspat_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/databrickspat"
26+
"github.com/google/osv-scalibr/veles/velestest"
27+
)
28+
29+
const testPAT = `dapi0123456789abcdef0123456789abcdef`
30+
const testPATWithSuffix = `dapi0123456789abcdef0123456789abcdef-2`
31+
32+
func TestDetectorAcceptance(t *testing.T) {
33+
velestest.AcceptDetector(
34+
t,
35+
databrickspat.NewDetector(),
36+
testPAT,
37+
databrickspat.UserAccountPAT{Token: testPAT},
38+
)
39+
}
40+
41+
func TestDetector_truePositives(t *testing.T) {
42+
engine, err := veles.NewDetectionEngine([]veles.Detector{databrickspat.NewDetector()})
43+
if err != nil {
44+
t.Fatal(err)
45+
}
46+
otherToken := testPAT[:len(testPAT)-1] + "0"
47+
cases := []struct {
48+
name string
49+
input string
50+
want []veles.Secret
51+
}{{
52+
name: "simple matching string",
53+
input: testPAT,
54+
want: []veles.Secret{
55+
databrickspat.UserAccountPAT{Token: testPAT},
56+
},
57+
}, {
58+
name: "optional single digit suffix",
59+
input: testPATWithSuffix,
60+
want: []veles.Secret{
61+
databrickspat.UserAccountPAT{Token: testPATWithSuffix},
62+
},
63+
}, {
64+
name: "match at end of env assignment",
65+
input: `DATABRICKS_TOKEN=` + testPAT,
66+
want: []veles.Secret{
67+
databrickspat.UserAccountPAT{Token: testPAT},
68+
},
69+
}, {
70+
name: "multiple distinct matches",
71+
input: testPAT + "\n" + otherToken,
72+
want: []veles.Secret{
73+
databrickspat.UserAccountPAT{Token: testPAT},
74+
databrickspat.UserAccountPAT{Token: otherToken},
75+
},
76+
}, {
77+
name: "larger input containing token",
78+
input: fmt.Sprintf(`
79+
[DEFAULT]
80+
token = "%s"
81+
`, testPAT),
82+
want: []veles.Secret{
83+
databrickspat.UserAccountPAT{Token: testPAT},
84+
},
85+
}}
86+
for _, tc := range cases {
87+
t.Run(tc.name, func(t *testing.T) {
88+
got, err := engine.Detect(t.Context(), strings.NewReader(tc.input))
89+
if err != nil {
90+
t.Errorf("Detect() error: %v, want nil", err)
91+
}
92+
if diff := cmp.Diff(tc.want, got, cmpopts.EquateEmpty()); diff != "" {
93+
t.Errorf("Detect() diff (-want +got):\n%s", diff)
94+
}
95+
})
96+
}
97+
}
98+
99+
func TestDetector_trueNegatives(t *testing.T) {
100+
engine, err := veles.NewDetectionEngine([]veles.Detector{databrickspat.NewDetector()})
101+
if err != nil {
102+
t.Fatal(err)
103+
}
104+
cases := []struct {
105+
name string
106+
input string
107+
want []veles.Secret
108+
}{{
109+
name: "empty input",
110+
input: "",
111+
}, {
112+
name: "short token should not match",
113+
input: testPAT[:len(testPAT)-1],
114+
}, {
115+
name: "long token should not match",
116+
input: testPAT + "0",
117+
}, {
118+
name: "invalid hex character should not match",
119+
input: testPAT[:len(testPAT)-1] + "g",
120+
}, {
121+
name: "uppercase hex should not match",
122+
input: strings.ToUpper(testPAT),
123+
}, {
124+
name: "incorrect prefix should not match",
125+
input: "dapo" + testPAT[len("dapi"):],
126+
}, {
127+
name: "two digit suffix should not match as full token",
128+
input: testPAT + "-20",
129+
}}
130+
for _, tc := range cases {
131+
t.Run(tc.name, func(t *testing.T) {
132+
got, err := engine.Detect(t.Context(), strings.NewReader(tc.input))
133+
if err != nil {
134+
t.Errorf("Detect() error: %v, want nil", err)
135+
}
136+
if diff := cmp.Diff(tc.want, got, cmpopts.EquateEmpty()); diff != "" {
137+
t.Errorf("Detect() diff (-want +got):\n%s", diff)
138+
}
139+
})
140+
}
141+
}

0 commit comments

Comments
 (0)