Skip to content

Commit 9f20212

Browse files
authored
feat: support path-based rule exclusions via exclude-rules (#1465)
* add path-based rule exclusions Implements #1287 * Ssupport for excluding specific rules from specific paths, enabling large monorepos to apply different security rules to different components (e.g., CLI tools vs services). * fix formatting issuue with path filter test to pass gci
1 parent 726d847 commit 9f20212

File tree

6 files changed

+772
-0
lines changed

6 files changed

+772
-0
lines changed

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,45 @@ A number of global settings can be provided in a configuration file as follows:
261261
$ gosec -conf config.json .
262262
```
263263

264+
### Path-Based Rule Exclusions
265+
266+
Large repositories with multiple components may need different security rules
267+
for different paths. Use `exclude-rules` to suppress specific rules for specific
268+
paths.
269+
270+
**Configuration File:**
271+
```json
272+
{
273+
"exclude-rules": [
274+
{
275+
"path": "cmd/.*",
276+
"rules": ["G204", "G304"]
277+
},
278+
{
279+
"path": "scripts/.*",
280+
"rules": ["*"]
281+
}
282+
]
283+
}
284+
```
285+
286+
**CLI Flag:**
287+
```bash
288+
# Exclude G204 and G304 from cmd/ directory
289+
gosec --exclude-rules="cmd/.*:G204,G304" ./...
290+
291+
# Exclude all rules from scripts/ directory
292+
gosec --exclude-rules="scripts/.*:*" ./...
293+
294+
# Multiple exclusions
295+
gosec --exclude-rules="cmd/.*:G204,G304;test/.*:G101" ./...
296+
```
297+
298+
| Field | Type | Description |
299+
|-------|------|-------------|
300+
| `path` | string (regex) | Regular expression matched against file paths |
301+
| `rules` | []string | Rule IDs to exclude. Use `*` to exclude all rules |
302+
264303
#### Rule Configuration
265304

266305
Some rules accept configuration flags as well; these flags are documented in [RULES.md](https://github.com/securego/gosec/blob/master/RULES.md).

cmd/gosec/main.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ USAGE:
5959
# Run all rules except the provided
6060
$ gosec -exclude=G101 $GOPATH/src/github.com/example/project/...
6161
62+
# Exclude specific rules from specific paths
63+
$ gosec --exclude-rules="cmd/.*:G204,G304" ./...
64+
65+
# Exclude all rules from scripts directory
66+
$ gosec --exclude-rules="scripts/.*:*" ./...
6267
`
6368
// Environment variable for AI API key.
6469
aiAPIKeyEnv = "GOSEC_AI_API_KEY" // #nosec G101
@@ -79,6 +84,12 @@ var (
7984
// #nosec flag
8085
flagIgnoreNoSec = flag.Bool("nosec", false, "Ignores #nosec comments when set")
8186

87+
// Path-based exclusions
88+
flagExcludeRules = flag.String("exclude-rules", "",
89+
`Path-based rule exclusions. Format: "path:rule1,rule2;path2:rule3"
90+
Example: "cmd/.*:G204,G304;test/.*:G101"
91+
Use "*" to exclude all rules for a path: "scripts/.*:*"`)
92+
8293
// show ignored
8394
flagShowIgnored = flag.Bool("show-ignored", false, "If enabled, ignored issues are printed")
8495

@@ -353,6 +364,27 @@ func exit(issues []*issue.Issue, errors map[string][]gosec.Error, noFail bool) {
353364
os.Exit(0)
354365
}
355366

367+
// buildPathExclusionFilter creates a PathExclusionFilter from config and CLI flags
368+
func buildPathExclusionFilter(config gosec.Config, cliFlag string) (*gosec.PathExclusionFilter, error) {
369+
// Parse CLI exclude-rules
370+
cliRules, err := gosec.ParseCLIExcludeRules(cliFlag)
371+
if err != nil {
372+
return nil, fmt.Errorf("invalid --exclude-rules flag: %w", err)
373+
}
374+
375+
// Get config file exclude-rules
376+
configRules, err := config.GetExcludeRules()
377+
if err != nil {
378+
return nil, fmt.Errorf("invalid exclude-rules in config: %w", err)
379+
}
380+
381+
// Merge rules (CLI takes precedence)
382+
allRules := gosec.MergeExcludeRules(configRules, cliRules)
383+
384+
// Create and return filter
385+
return gosec.NewPathExclusionFilter(allRules)
386+
}
387+
356388
func main() {
357389
// Makes sure some version information is set
358390
prepareVersionInfo()
@@ -440,6 +472,12 @@ func main() {
440472
logger.Fatal("No rules/analyzers are configured")
441473
}
442474

475+
// Build path exclusion filter
476+
pathFilter, err := buildPathExclusionFilter(config, *flagExcludeRules)
477+
if err != nil {
478+
logger.Fatalf("Path exclusion filter error: %v", err)
479+
}
480+
443481
// Create the analyzer
444482
analyzer := gosec.NewAnalyzer(config, *flagScanTests, *flagExcludeGenerated, *flagTrackSuppressions, *flagConcurrency, logger)
445483
analyzer.LoadRules(ruleList.RulesInfo())
@@ -476,6 +514,13 @@ func main() {
476514
// Collect the results
477515
issues, metrics, errors := analyzer.Report()
478516

517+
// Apply path-based exclusions first
518+
var pathExcludedCount int
519+
issues, pathExcludedCount = pathFilter.FilterIssues(issues)
520+
if pathExcludedCount > 0 {
521+
logger.Printf("Excluded %d issues by path-based rules", pathExcludedCount)
522+
}
523+
479524
// Sort the issue by severity
480525
if *flagSortIssues {
481526
sortIssues(issues)

config.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const (
1111
// Globals are applicable to all rules and used for general
1212
// configuration settings for gosec.
1313
Globals = "global"
14+
// ExcludeRulesKey is the config key for path-based rule exclusions
15+
ExcludeRulesKey = "exclude-rules"
1416
)
1517

1618
// GlobalOption defines the name of the global options
@@ -135,3 +137,38 @@ func (c Config) IsGlobalEnabled(option GlobalOption) (bool, error) {
135137
}
136138
return (value == "true" || value == "enabled"), nil
137139
}
140+
141+
// GetExcludeRules retrieves the path-based exclusion rules from the configuration.
142+
// Returns nil if no exclusion rules are configured.
143+
func (c Config) GetExcludeRules() ([]PathExcludeRule, error) {
144+
if c == nil {
145+
return nil, nil
146+
}
147+
148+
rawRules, exists := c[ExcludeRulesKey]
149+
if !exists {
150+
return nil, nil
151+
}
152+
153+
// The config is unmarshaled as map[string]interface{}, so we need to
154+
// re-marshal and unmarshal to get the proper typed struct
155+
rulesJSON, err := json.Marshal(rawRules)
156+
if err != nil {
157+
return nil, fmt.Errorf("failed to marshal exclude-rules: %w", err)
158+
}
159+
160+
var rules []PathExcludeRule
161+
if err := json.Unmarshal(rulesJSON, &rules); err != nil {
162+
return nil, fmt.Errorf("failed to parse exclude-rules: %w", err)
163+
}
164+
165+
return rules, nil
166+
}
167+
168+
// SetExcludeRules sets the path-based exclusion rules in the configuration.
169+
func (c Config) SetExcludeRules(rules []PathExcludeRule) {
170+
if c == nil {
171+
return
172+
}
173+
c[ExcludeRulesKey] = rules
174+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"global": {
3+
"audit": false,
4+
"nosec": false,
5+
"show-ignored": false
6+
},
7+
"G101": {
8+
"pattern": "(?i)passwd|pass|password|pwd|secret|private_key|token|api_key",
9+
"ignore_entropy": false,
10+
"entropy_threshold": "80.0",
11+
"per_char_threshold": "3.0",
12+
"truncate": "32"
13+
},
14+
"exclude-rules": [
15+
{
16+
"path": "cmd/.*",
17+
"rules": ["G204", "G304"]
18+
},
19+
{
20+
"path": "internal/testutil/.*",
21+
"rules": ["G101", "G401", "G501"]
22+
},
23+
{
24+
"path": "scripts/.*",
25+
"rules": ["*"]
26+
},
27+
{
28+
"path": ".*_test\\.go$",
29+
"rules": ["G101", "G304"]
30+
},
31+
{
32+
"path": "internal/(mock|fake|stub)s?/.*",
33+
"rules": ["*"]
34+
}
35+
]
36+
}

0 commit comments

Comments
 (0)