1
- package azuredevopspersonalaccesstoken
1
+ package azure_devops
2
2
3
3
import (
4
4
"context"
5
+ "encoding/json"
6
+ "errors"
5
7
"fmt"
8
+ "io"
6
9
"net/http"
7
10
"strings"
8
11
9
12
regexp "github.com/wasilibs/go-re2"
10
13
14
+ "github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
11
15
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
12
16
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
13
17
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
@@ -21,75 +25,143 @@ type Scanner struct {
21
25
// Ensure the Scanner satisfies the interface at compile time.
22
26
var _ detectors.Detector = (* Scanner )(nil )
23
27
24
- var (
25
- defaultClient = common .SaneHttpClient ()
26
- // Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
27
- keyPat = regexp .MustCompile (detectors .PrefixRegex ([]string {"azure" }) + `\b([0-9a-z]{52})\b` )
28
- orgPat = regexp .MustCompile (detectors .PrefixRegex ([]string {"azure" }) + `\b([0-9a-zA-Z][0-9a-zA-Z-]{5,48}[0-9a-zA-Z])\b` )
29
- )
28
+ func (s Scanner ) Type () detectorspb.DetectorType {
29
+ return detectorspb .DetectorType_AzureDevopsPersonalAccessToken
30
+ }
31
+
32
+ func (s Scanner ) Description () string {
33
+ return "Azure DevOps is a suite of development tools provided by Microsoft. Personal Access Tokens (PATs) are used to authenticate and authorize access to Azure DevOps services and resources."
34
+ }
30
35
31
36
// Keywords are used for efficiently pre-filtering chunks.
32
37
// Use identifiers in the secret preferably, or the provider name.
33
38
func (s Scanner ) Keywords () []string {
34
- return []string {"azure" }
39
+ return []string {"dev. azure.com" , "az devops " }
35
40
}
36
41
42
+ var (
43
+ // Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
44
+ keyPat = regexp .MustCompile (detectors .PrefixRegex ([]string {"azure" , "az" , "token" , "pat" }) + `\b([a-z0-9]{52}|[a-zA-Z0-9]{84})\b` )
45
+ orgPat = regexp .MustCompile (`dev\.azure\.com/([0-9a-zA-Z][0-9a-zA-Z-]{5,48}[0-9a-zA-Z])\b` )
46
+
47
+ invalidOrgCache = simple .NewCache [struct {}]()
48
+ )
49
+
37
50
// FromData will find and optionally verify AzureDevopsPersonalAccessToken secrets in a given set of bytes.
38
51
func (s Scanner ) FromData (ctx context.Context , verify bool , data []byte ) (results []detectors.Result , err error ) {
39
52
dataStr := string (data )
40
53
41
- matches := keyPat .FindAllStringSubmatch (dataStr , - 1 )
42
- orgMatches := orgPat .FindAllStringSubmatch (dataStr , - 1 )
43
-
44
- for _ , match := range matches {
45
- resMatch := strings .TrimSpace (match [1 ])
46
- for _ , orgMatch := range orgMatches {
47
- resOrgMatch := strings .TrimSpace (orgMatch [1 ])
54
+ // Deduplicate results.
55
+ keyMatches := make (map [string ]struct {})
56
+ for _ , match := range keyPat .FindAllStringSubmatch (dataStr , - 1 ) {
57
+ m := match [1 ]
58
+ if detectors .StringShannonEntropy (m ) < 3 {
59
+ continue
60
+ }
61
+ keyMatches [m ] = struct {}{}
62
+ }
63
+ orgMatches := make (map [string ]struct {})
64
+ for _ , match := range orgPat .FindAllStringSubmatch (dataStr , - 1 ) {
65
+ m := match [1 ]
66
+ if invalidOrgCache .Exists (m ) {
67
+ continue
68
+ }
69
+ orgMatches [m ] = struct {}{}
70
+ }
48
71
49
- s1 := detectors.Result {
72
+ for key := range keyMatches {
73
+ for org := range orgMatches {
74
+ r := detectors.Result {
50
75
DetectorType : detectorspb .DetectorType_AzureDevopsPersonalAccessToken ,
51
- Raw : []byte (resMatch ),
52
- RawV2 : []byte (resMatch + resOrgMatch ),
76
+ Raw : []byte (key ),
77
+ RawV2 : []byte (fmt . Sprintf ( `{"organization":"%s","token":"%s"}` , org , key ) ),
53
78
}
54
79
55
80
if verify {
56
- client := s .client
57
- if client == nil {
58
- client = defaultClient
59
- }
60
- req , err := http .NewRequestWithContext (ctx , "GET" , "https://dev.azure.com/" + resOrgMatch + "/_apis/projects" , nil )
61
- if err != nil {
62
- continue
81
+ if s .client == nil {
82
+ s .client = common .SaneHttpClient ()
63
83
}
64
- req .SetBasicAuth ("" , resMatch )
65
- res , err := client .Do (req )
66
- if err == nil {
67
- defer res .Body .Close ()
68
- hasVerifiedRes , _ := common .ResponseContainsSubstring (res .Body , "lastUpdateTime" )
69
- if res .StatusCode >= 200 && res .StatusCode < 300 && hasVerifiedRes {
70
- s1 .Verified = true
71
- } else if res .StatusCode == 401 {
72
- // The secret is determinately not verified (nothing to do)
73
- } else {
74
- err = fmt .Errorf ("unexpected HTTP response status %d" , res .StatusCode )
75
- s1 .SetVerificationError (err , resMatch )
84
+
85
+ isVerified , extraData , verificationErr := verifyMatch (ctx , s .client , org , key )
86
+ r .Verified = isVerified
87
+ r .ExtraData = extraData
88
+ if verificationErr != nil {
89
+ if errors .Is (verificationErr , errInvalidOrg ) {
90
+ delete (orgMatches , org )
91
+ invalidOrgCache .Set (org , struct {}{})
92
+ continue
76
93
}
77
- } else {
78
- s1 .SetVerificationError (err , resMatch )
94
+ r .SetVerificationError (verificationErr )
79
95
}
80
96
}
81
97
82
- results = append (results , s1 )
98
+ results = append (results , r )
83
99
}
84
100
}
85
101
86
102
return results , nil
87
103
}
88
104
89
- func (s Scanner ) Type () detectorspb.DetectorType {
90
- return detectorspb .DetectorType_AzureDevopsPersonalAccessToken
105
+ var errInvalidOrg = errors .New ("invalid organization" )
106
+
107
+ func verifyMatch (ctx context.Context , client * http.Client , org string , key string ) (bool , map [string ]string , error ) {
108
+ req , err := http .NewRequestWithContext (ctx , "GET" , "https://dev.azure.com/" + org + "/_apis/projects" , nil )
109
+ if err != nil {
110
+ return false , nil , err
111
+ }
112
+
113
+ req .SetBasicAuth ("" , key )
114
+ req .Header .Set ("Accept" , "application/json" )
115
+ req .Header .Set ("Content-Type" , "application/json" )
116
+ res , err := client .Do (req )
117
+ if err != nil {
118
+ return false , nil , err
119
+ }
120
+ defer func () {
121
+ _ , _ = io .Copy (io .Discard , res .Body )
122
+ _ = res .Body .Close ()
123
+ }()
124
+
125
+ switch res .StatusCode {
126
+ case http .StatusOK :
127
+ // {"count":1,"value":[{"id":"...","name":"Test","url":"https://dev.azure.com/...","state":"wellFormed","revision":11,"visibility":"private","lastUpdateTime":"2024-12-16T02:23:58.86Z"}]}
128
+ var projectsRes listProjectsResponse
129
+ if json .NewDecoder (res .Body ).Decode (& projectsRes ) != nil {
130
+ return false , nil , err
131
+ }
132
+
133
+ // Condense a list of organizations + roles.
134
+ var (
135
+ extraData map [string ]string
136
+ projects = make ([]string , 0 , len (projectsRes .Value ))
137
+ )
138
+ for _ , p := range projectsRes .Value {
139
+ projects = append (projects , p .Name )
140
+ }
141
+ if len (projects ) > 0 {
142
+ extraData = map [string ]string {
143
+ "projects" : strings .Join (projects , "," ),
144
+ }
145
+ }
146
+ return true , extraData , nil
147
+ case http .StatusUnauthorized :
148
+ // The secret is determinately not verified (nothing to do)
149
+ return false , nil , nil
150
+ case http .StatusNotFound :
151
+ // Org doesn't exist.
152
+ return false , nil , errInvalidOrg
153
+ default :
154
+ body , _ := io .ReadAll (res .Body )
155
+ return false , nil , fmt .Errorf ("unexpected HTTP response: status=%d, body=%q" , res .StatusCode , string (body ))
156
+ }
91
157
}
92
158
93
- func (s Scanner ) Description () string {
94
- return "Azure DevOps is a suite of development tools provided by Microsoft. Personal Access Tokens (PATs) are used to authenticate and authorize access to Azure DevOps services and resources."
159
+ type listProjectsResponse struct {
160
+ Count int `json:"count"`
161
+ Value []projectResponse `json:"value"`
162
+ }
163
+
164
+ type projectResponse struct {
165
+ Id string `json:"id"`
166
+ Name string `json:"name"`
95
167
}
0 commit comments