diff --git a/internal/modules/credential_exposure_test.go b/internal/modules/credential_exposure_test.go
new file mode 100644
index 0000000..b5e0b84
--- /dev/null
+++ b/internal/modules/credential_exposure_test.go
@@ -0,0 +1,113 @@
+package modules_test
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/dropalldatabases/sif/internal/modules"
+)
+
+func runCredModule(t *testing.T, file string, status int, body string) *modules.Result {
+ t.Helper()
+ def, err := modules.ParseYAMLModule(file)
+ if err != nil {
+ t.Fatalf("parse %s: %v", file, err)
+ }
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(status)
+ _, _ = w.Write([]byte(body))
+ }))
+ defer srv.Close()
+
+ res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
+ Timeout: 5 * time.Second,
+ Threads: 2,
+ })
+ if err != nil {
+ t.Fatalf("execute %s: %v", file, err)
+ }
+ return res
+}
+
+func credExtract(res *modules.Result, key string) string {
+ for _, f := range res.Findings {
+ if v := f.Extracted[key]; v != "" {
+ return v
+ }
+ }
+ return ""
+}
+
+func TestCredentialExposureModules(t *testing.T) {
+ const aws = "../../modules/recon/aws-credentials-exposure.yaml"
+ const npmrc = "../../modules/recon/npmrc-exposure.yaml"
+ const docker = "../../modules/recon/docker-config-exposure.yaml"
+
+ t.Run("aws credentials leak the access key id", func(t *testing.T) {
+ body := "[default]\naws_access_key_id = AKIAIOSFODNN7EXAMPLE\n" +
+ "aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n"
+ res := runCredModule(t, aws, 200, body)
+ if len(res.Findings) == 0 {
+ t.Fatal("expected an aws credentials finding")
+ }
+ if v := credExtract(res, "aws_access_key_id"); v != "AKIAIOSFODNN7EXAMPLE" {
+ t.Errorf("aws_access_key_id=%q, want AKIAIOSFODNN7EXAMPLE", v)
+ }
+ })
+
+ t.Run("npmrc leaks the registry of an auth token", func(t *testing.T) {
+ body := "//registry.npmjs.org/:_authToken=npm_AbCdEf0123456789AbCdEf0123456789\n"
+ res := runCredModule(t, npmrc, 200, body)
+ if len(res.Findings) == 0 {
+ t.Fatal("expected an npmrc finding")
+ }
+ if v := credExtract(res, "npm_registry"); v != "registry.npmjs.org" {
+ t.Errorf("npm_registry=%q, want registry.npmjs.org", v)
+ }
+ })
+
+ t.Run("docker config leaks the registry host", func(t *testing.T) {
+ body := `{"auths":{"registry.example.com":{"auth":"dXNlcm5hbWU6c3VwZXJzZWNyZXRwYXNz"}}}`
+ res := runCredModule(t, docker, 200, body)
+ if len(res.Findings) == 0 {
+ t.Fatal("expected a docker config finding")
+ }
+ if v := credExtract(res, "docker_registry"); v != "registry.example.com" {
+ t.Errorf("docker_registry=%q, want registry.example.com", v)
+ }
+ })
+
+ t.Run("html page mentioning the key name is not a leak", func(t *testing.T) {
+ body := `
Docs` +
+ `set your aws_secret_access_key in ~/.aws/credentials`
+ if res := runCredModule(t, aws, 200, body); len(res.Findings) > 0 {
+ t.Errorf("an html doc mentioning the key should not match, got %d findings", len(res.Findings))
+ }
+ })
+
+ t.Run("a 404 is not a leak", func(t *testing.T) {
+ for _, file := range []string{aws, npmrc, docker} {
+ if res := runCredModule(t, file, 404, "not found"); len(res.Findings) > 0 {
+ t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
+ }
+ }
+ })
+
+ t.Run("a plain 200 body is not a leak", func(t *testing.T) {
+ for _, file := range []string{aws, npmrc, docker} {
+ if res := runCredModule(t, file, 200, "nothing to see here"); len(res.Findings) > 0 {
+ t.Errorf("%s: a plain 200 body should not match, got %d findings", file, len(res.Findings))
+ }
+ }
+ })
+
+ t.Run("a docker auth field holding a jwt is not a leak", func(t *testing.T) {
+ body := `{"token":"x","auth":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"}`
+ if res := runCredModule(t, docker, 200, body); len(res.Findings) > 0 {
+ t.Errorf("a jwt in an auth field should not match, got %d findings", len(res.Findings))
+ }
+ })
+}
diff --git a/modules/recon/aws-credentials-exposure.yaml b/modules/recon/aws-credentials-exposure.yaml
new file mode 100644
index 0000000..15791ee
--- /dev/null
+++ b/modules/recon/aws-credentials-exposure.yaml
@@ -0,0 +1,53 @@
+# AWS Credentials File Exposure Detection Module
+
+id: aws-credentials-exposure
+info:
+ name: AWS Credentials File Exposure
+ author: sif
+ severity: high
+ description: Detects exposed AWS credential files that leak access key ids and secret keys
+ tags: [aws, credentials, secrets, exposure, recon]
+
+type: http
+
+http:
+ method: GET
+ paths:
+ - "{{BaseURL}}/.aws/credentials"
+ - "{{BaseURL}}/.s3cfg"
+ - "{{BaseURL}}/.boto"
+
+ threads: 3
+
+ matchers:
+ - type: status
+ status:
+ - 200
+
+ - type: word
+ part: body
+ condition: or
+ words:
+ - "aws_secret_access_key"
+ - "aws_access_key_id"
+ - "secret_key"
+
+ - type: word
+ part: body
+ negative: true
+ condition: or
+ words:
+ - ""
+ - ""
+
+ extractors:
+ - type: regex
+ name: aws_access_key_id
+ part: body
+ regex:
+ - '((?:AKIA|ASIA)[0-9A-Z]{16})'
+ group: 1
diff --git a/modules/recon/docker-config-exposure.yaml b/modules/recon/docker-config-exposure.yaml
new file mode 100644
index 0000000..7cfe576
--- /dev/null
+++ b/modules/recon/docker-config-exposure.yaml
@@ -0,0 +1,47 @@
+# Docker Config Credential Exposure Detection Module
+
+id: docker-config-exposure
+info:
+ name: Docker Config Credential Exposure
+ author: sif
+ severity: high
+ description: Detects exposed docker config files that leak base64 encoded registry credentials
+ tags: [docker, registry, credentials, secrets, exposure, recon]
+
+type: http
+
+http:
+ method: GET
+ paths:
+ - "{{BaseURL}}/.docker/config.json"
+ - "{{BaseURL}}/.dockercfg"
+
+ matchers:
+ - type: status
+ status:
+ - 200
+
+ - type: regex
+ part: body
+ regex:
+ - '"auth"\s*:\s*"[A-Za-z0-9+/=]{20,}"'
+
+ - type: word
+ part: body
+ negative: true
+ condition: or
+ words:
+ - ""
+ - ""
+
+ extractors:
+ - type: regex
+ name: docker_registry
+ part: body
+ regex:
+ - '"auths"\s*:\s*\{\s*"([^"]+)"'
+ group: 1
diff --git a/modules/recon/npmrc-exposure.yaml b/modules/recon/npmrc-exposure.yaml
new file mode 100644
index 0000000..13559c9
--- /dev/null
+++ b/modules/recon/npmrc-exposure.yaml
@@ -0,0 +1,50 @@
+# npmrc Token Exposure Detection Module
+
+id: npmrc-exposure
+info:
+ name: npmrc Token Exposure
+ author: sif
+ severity: high
+ description: Detects exposed .npmrc files that leak registry auth tokens or passwords
+ tags: [npm, npmrc, token, secrets, exposure, recon]
+
+type: http
+
+http:
+ method: GET
+ paths:
+ - "{{BaseURL}}/.npmrc"
+
+ matchers:
+ - type: status
+ status:
+ - 200
+
+ - type: word
+ part: body
+ condition: or
+ words:
+ - "_authToken"
+ - "_auth="
+ - "_auth ="
+ - ":_password"
+
+ - type: word
+ part: body
+ negative: true
+ condition: or
+ words:
+ - ""
+ - ""
+
+ extractors:
+ - type: regex
+ name: npm_registry
+ part: body
+ regex:
+ - '//([^/:]+)/:_authToken'
+ group: 1