|
1 | 1 | package tools |
2 | 2 |
|
3 | | -import "testing" |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "testing" |
| 6 | +) |
4 | 7 |
|
5 | 8 | func TestDetectShellOperators(t *testing.T) { |
6 | 9 | tests := []struct { |
@@ -120,3 +123,85 @@ func TestParseCommandBinary(t *testing.T) { |
120 | 123 | }) |
121 | 124 | } |
122 | 125 | } |
| 126 | + |
| 127 | +// TestMatchesBinaryVerbose verifies start-anchored per-arg matching for |
| 128 | +// deny_verbose patterns. Regression guard: `-v` must NOT false-positive on |
| 129 | +// `--version` (used by the system to probe CLI availability), but MUST still |
| 130 | +// block real verbose flags (`-v`, `-vv`, `-v=1`, `--verbose=true`) to prevent |
| 131 | +// leakage of tokens/request bodies via verbose output. |
| 132 | +func TestMatchesBinaryVerbose(t *testing.T) { |
| 133 | + ghPatterns, _ := json.Marshal([]string{"--verbose", "-v"}) |
| 134 | + gcloudPatterns, _ := json.Marshal([]string{"--verbosity=debug", "--log-http"}) |
| 135 | + awsPatterns, _ := json.Marshal([]string{"--debug"}) |
| 136 | + |
| 137 | + tests := []struct { |
| 138 | + name string |
| 139 | + patterns json.RawMessage |
| 140 | + args []string |
| 141 | + wantHit bool |
| 142 | + }{ |
| 143 | + // --- regression: safe flags must pass --- |
| 144 | + {"gh --version not blocked", ghPatterns, []string{"--version"}, false}, |
| 145 | + {"gh version subcmd not blocked", ghPatterns, []string{"version"}, false}, |
| 146 | + {"gh --help not blocked", ghPatterns, []string{"--help"}, false}, |
| 147 | + {"gh api repos/x not blocked", ghPatterns, []string{"api", "repos/x"}, false}, |
| 148 | + |
| 149 | + // --- real verbose flags still blocked --- |
| 150 | + {"gh -v blocked", ghPatterns, []string{"-v"}, true}, |
| 151 | + {"gh --verbose blocked", ghPatterns, []string{"--verbose"}, true}, |
| 152 | + {"gh -vv blocked (escalation)", ghPatterns, []string{"-vv"}, true}, |
| 153 | + {"gh -vvv blocked (escalation)", ghPatterns, []string{"-vvv"}, true}, |
| 154 | + {"gh --verbose=true blocked (equals form)", ghPatterns, []string{"--verbose=true"}, true}, |
| 155 | + {"gh -v in middle of args blocked", ghPatterns, []string{"api", "-v", "repos/x"}, true}, |
| 156 | + |
| 157 | + // --- gcloud patterns: exact flag=value --- |
| 158 | + {"gcloud --verbosity=debug blocked", gcloudPatterns, []string{"--verbosity=debug"}, true}, |
| 159 | + {"gcloud --verbosity=info not blocked", gcloudPatterns, []string{"--verbosity=info"}, false}, |
| 160 | + {"gcloud --log-http blocked", gcloudPatterns, []string{"--log-http"}, true}, |
| 161 | + {"gcloud version not blocked", gcloudPatterns, []string{"version"}, false}, |
| 162 | + |
| 163 | + // --- aws --- |
| 164 | + {"aws --debug blocked", awsPatterns, []string{"--debug"}, true}, |
| 165 | + {"aws --debugger not blocked-worthy (prefix match)", awsPatterns, []string{"--debugger"}, true}, // acceptable: still debug family |
| 166 | + {"aws --version not blocked", awsPatterns, []string{"--version"}, false}, |
| 167 | + |
| 168 | + // --- empty / no patterns --- |
| 169 | + {"empty patterns", json.RawMessage(nil), []string{"--verbose"}, false}, |
| 170 | + {"empty args", ghPatterns, []string{}, false}, |
| 171 | + } |
| 172 | + for _, tt := range tests { |
| 173 | + t.Run(tt.name, func(t *testing.T) { |
| 174 | + got := matchesBinaryVerbose(tt.args, tt.patterns) |
| 175 | + if (got != "") != tt.wantHit { |
| 176 | + t.Errorf("matchesBinaryVerbose(%v) = %q, wantHit=%v", tt.args, got, tt.wantHit) |
| 177 | + } |
| 178 | + }) |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +// TestMatchesBinaryDenyJoinedArgs verifies deny_args keeps joined-string |
| 183 | +// matching so multi-token patterns like `auth\s+login` and `repo\s+delete` |
| 184 | +// still work. |
| 185 | +func TestMatchesBinaryDenyJoinedArgs(t *testing.T) { |
| 186 | + ghPatterns, _ := json.Marshal([]string{`auth\s+`, `repo\s+delete`, `secret\s+`}) |
| 187 | + |
| 188 | + tests := []struct { |
| 189 | + name string |
| 190 | + args []string |
| 191 | + wantHit bool |
| 192 | + }{ |
| 193 | + {"gh auth login blocked", []string{"auth", "login"}, true}, |
| 194 | + {"gh repo delete blocked", []string{"repo", "delete", "foo/bar"}, true}, |
| 195 | + {"gh secret set blocked", []string{"secret", "set", "TOKEN"}, true}, |
| 196 | + {"gh api repos allowed", []string{"api", "repos/x"}, false}, |
| 197 | + {"gh repo view allowed", []string{"repo", "view", "foo/bar"}, false}, |
| 198 | + } |
| 199 | + for _, tt := range tests { |
| 200 | + t.Run(tt.name, func(t *testing.T) { |
| 201 | + got := matchesBinaryDeny(tt.args, ghPatterns) |
| 202 | + if (got != "") != tt.wantHit { |
| 203 | + t.Errorf("matchesBinaryDeny(%v) = %q, wantHit=%v", tt.args, got, tt.wantHit) |
| 204 | + } |
| 205 | + }) |
| 206 | + } |
| 207 | +} |
0 commit comments