diff --git a/README.mkd b/README.mkd index ab407f9..4cbf054 100644 --- a/README.mkd +++ b/README.mkd @@ -25,30 +25,19 @@ such as being assigned to `document.location`, passed to `window.open()`, or pas A simple example program is provided [here](/examples/basic/main.go): ```go -package main +analyzer := jsluice.NewAnalyzer([]byte(` + const login = (redirect) => { + document.location = "/login?redirect=" + redirect + "&method=oauth" + } +`)) -import ( - "encoding/json" - "fmt" +for _, url := range analyzer.GetURLs() { + j, err := json.MarshalIndent(url, "", " ") + if err != nil { + continue + } - "github.com/BishopFox/jsluice" -) - -func main() { - analyzer := jsluice.NewAnalyzer([]byte(` - const login = (redirect) => { - document.location = "/login?redirect=" + redirect + "&method=oauth" - } - `)) - - for _, url := range analyzer.GetURLs() { - j, err := json.MarshalIndent(url, "", " ") - if err != nil { - continue - } - - fmt.Printf("%s\n", j) - } + fmt.Printf("%s\n", j) } ``` @@ -89,47 +78,38 @@ document.location = "/login?redirect=" ### Custom URL Matchers -`jsluice` comes with some built-in URL matchers for common scenarios, but you can easily add more +`jsluice` comes with some built-in URL matchers for common scenarios, but you can add more with the `AddURLMatcher` function: ```go -package main +analyzer := jsluice.NewAnalyzer([]byte(` + var fn = () => { + var meta = { + contact: "mailto:contact@example.com", + home: "https://example.com" + } + return meta + } +`)) -import ( - "fmt" - "strings" +analyzer.AddURLMatcher( + // The first value in the jsluice.URLMatcher struct is the type of node to look for. + // It can be one of "string", "assignment_expression", or "call_expression" + jsluice.URLMatcher{"string", func(n *jsluice.Node) *jsluice.URL { + val := n.DecodedString() + if !strings.HasPrefix(val, "mailto:") { + return nil + } - "github.com/BishopFox/jsluice" + return &jsluice.URL{ + URL: val, + Type: "mailto", + } + }}, ) -func main() { - analyzer := jsluice.NewAnalyzer([]byte(` - var fn = () => { - var meta = { - contact: "mailto:contact@example.com", - home: "https://example.com" - } - return meta - } - `)) - - analyzer.AddURLMatcher( - jsluice.URLMatcher{"string", func(n *jsluice.Node) *jsluice.URL { - val := n.DecodedString() - if !strings.HasPrefix(val, "mailto:") { - return nil - } - - return &jsluice.URL{ - URL: val, - Type: "mailto", - } - }}, - ) - - for _, match := range analyzer.GetURLs() { - fmt.Println(match.URL) - } +for _, match := range analyzer.GetURLs() { + fmt.Println(match.URL) } ``` @@ -144,3 +124,53 @@ https://example.com `jsluice` doesn't match `mailto:` URIs by default, it was found by the custom `URLMatcher`. +## Extracting Secrets + +As well as URLs, `jsluice` can extract secrets. As with URL extraction, custom matchers can +be supplied to supplement the default matchers. There's a short example program [here](/examples/secrets/main.go) +that does just that: + +```go +analyzer := jsluice.NewAnalyzer([]byte(` + var config = { + apiKey: "AUTH_1a2b3c4d5e6f", + apiURL: "https://api.example.com/v2/" + } +`)) + +analyzer.AddSecretMatcher( + // The first value in the jsluice.SecretMatcher struct is a + // tree-sitter query to run on the JavaScript source. + jsluice.SecretMatcher{"(pair) @match", func(n *jsluice.Node) *jsluice.Secret { + key := n.ChildByFieldName("key").DecodedString() + value := n.ChildByFieldName("value").DecodedString() + + if !strings.Contains(key, "api") { + return nil + } + + if !strings.HasPrefix(value, "AUTH_") { + return nil + } + + return &jsluice.Secret{ + Kind: "fakeApi", + Data: map[string]string{ + "key": key, + "value": value, + }, + Severity: jsluice.SeverityLow, + Context: n.Parent().AsMap(), + } + }}, +) + +for _, match := range analyzer.GetSecrets() { + j, err := json.MarshalIndent(match, "", " ") + if err != nil { + continue + } + + fmt.Printf("%s\n", j) +} +``` diff --git a/examples/secrets/main.go b/examples/secrets/main.go new file mode 100644 index 0000000..af7fb80 --- /dev/null +++ b/examples/secrets/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/BishopFox/jsluice" +) + +func main() { + analyzer := jsluice.NewAnalyzer([]byte(` + var config = { + apiKey: "AUTH_1a2b3c4d5e6f", + apiURL: "https://api.example.com/v2/" + } + `)) + + analyzer.AddSecretMatcher( + // The first value in the jsluice.SecretMatcher struct is a + // tree-sitter query to run on the JavaScript source. + jsluice.SecretMatcher{"(pair) @match", func(n *jsluice.Node) *jsluice.Secret { + key := n.ChildByFieldName("key").DecodedString() + value := n.ChildByFieldName("value").DecodedString() + + if !strings.Contains(key, "api") { + return nil + } + + if !strings.HasPrefix(value, "AUTH_") { + return nil + } + + return &jsluice.Secret{ + Kind: "fakeApi", + Data: map[string]string{ + "key": key, + "value": value, + }, + Severity: jsluice.SeverityLow, + Context: n.Parent().AsMap(), + } + }}, + ) + + for _, match := range analyzer.GetSecrets() { + j, err := json.MarshalIndent(match, "", " ") + if err != nil { + continue + } + + fmt.Printf("%s\n", j) + } +} diff --git a/examples/urlmatcher/main.go b/examples/urlmatcher/main.go index eb7166a..839ce46 100644 --- a/examples/urlmatcher/main.go +++ b/examples/urlmatcher/main.go @@ -19,6 +19,8 @@ func main() { `)) analyzer.AddURLMatcher( + // The first value in the jsluice.URLMatcher struct is the type of node to look for. + // It can be one of "string", "assignment_expression", or "call_expression" jsluice.URLMatcher{"string", func(n *jsluice.Node) *jsluice.URL { val := n.DecodedString() if !strings.HasPrefix(val, "mailto:") { diff --git a/secret-matchers.go b/secret-matchers.go index c7302e5..c617193 100644 --- a/secret-matchers.go +++ b/secret-matchers.go @@ -7,11 +7,11 @@ import ( // A Secret represents any secret or otherwise interesting data // found within a JavaScript file. E.g. an AWS access key. type Secret struct { - Kind string `json:"kind"` - Data any `json:"data"` - Filename string `json:"filename,omitempty"` - Severity Severity `json:"severity"` - Context map[string]string `json:"context"` + Kind string `json:"kind"` + Data any `json:"data"` + Filename string `json:"filename,omitempty"` + Severity Severity `json:"severity"` + Context any `json:"context"` } // Severity indicates how serious a finding is diff --git a/user-patterns_test.go b/user-patterns_test.go index 5035615..c2ac6fa 100644 --- a/user-patterns_test.go +++ b/user-patterns_test.go @@ -7,8 +7,8 @@ import ( func TestParseUserPatterns(t *testing.T) { testData := strings.NewReader(`[ - {"name": "httpAuth", "pattern": "/[a-z0-9_/\\.:-]+@[a-z0-9-]+\\.[a-z0-9.-]+"}, - {"name": "base64", "pattern": "^(eyJ|YTo|Tzo|PD[89]|aHR0cHM6L|aHR0cDo|rO0)[%a-zA-Z0-9+/]+={0,2}"} + {"name": "httpAuth", "value": "/[a-z0-9_/\\.:-]+@[a-z0-9-]+\\.[a-z0-9.-]+"}, + {"name": "base64", "value": "^(eyJ|YTo|Tzo|PD[89]|aHR0cHM6L|aHR0cDo|rO0)[%a-zA-Z0-9+/]+={0,2}"} ]`) patterns, err := ParseUserPatterns(testData) @@ -35,10 +35,10 @@ func TestParseUserPatterns(t *testing.T) { } for _, c := range cases { - if patterns[c.i].Match(c.in) != c.expected { + if patterns[c.i].MatchValue(c.in) != c.expected { t.Errorf( - "Want %t for (%s).Match(%s); have %t", - c.expected, patterns[c.i].Pattern, c.in, !c.expected, + "Want %t for (%s).MatchValue(%s); have %t", + c.expected, patterns[c.i].reValue, c.in, !c.expected, ) } }