config.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. package config
  2. import (
  3. _ "embed"
  4. "errors"
  5. "fmt"
  6. "sort"
  7. "strings"
  8. "github.com/spf13/viper"
  9. "github.com/zricethezav/gitleaks/v8/logging"
  10. "github.com/zricethezav/gitleaks/v8/regexp"
  11. )
  12. var (
  13. //go:embed gitleaks.toml
  14. DefaultConfig string
  15. // use to keep track of how many configs we can extend
  16. // yea I know, globals bad
  17. extendDepth int
  18. )
  19. const maxExtendDepth = 2
  20. // ViperConfig is the config struct used by the Viper config package
  21. // to parse the config file. This struct does not include regular expressions.
  22. // It is used as an intermediary to convert the Viper config to the Config struct.
  23. type ViperConfig struct {
  24. Title string
  25. Description string
  26. Extend Extend
  27. Rules []struct {
  28. ID string
  29. Description string
  30. Path string
  31. Regex string
  32. SecretGroup int
  33. Entropy float64
  34. Keywords []string
  35. Tags []string
  36. // Deprecated: this is a shim for backwards-compatibility.
  37. // TODO: Remove this in 9.x.
  38. AllowList *viperRuleAllowlist
  39. Allowlists []*viperRuleAllowlist
  40. Required []*viperRequired
  41. SkipReport bool
  42. SmartFilter bool
  43. }
  44. // Deprecated: this is a shim for backwards-compatibility.
  45. // TODO: Remove this in 9.x.
  46. AllowList *viperGlobalAllowlist
  47. Allowlists []*viperGlobalAllowlist
  48. }
  49. type viperRequired struct {
  50. ID string
  51. WithinLines *int `mapstructure:"withinLines"`
  52. WithinColumns *int `mapstructure:"withinColumns"`
  53. }
  54. type viperRuleAllowlist struct {
  55. Description string
  56. Condition string
  57. Commits []string
  58. Paths []string
  59. RegexTarget string
  60. Regexes []string
  61. StopWords []string
  62. }
  63. type viperGlobalAllowlist struct {
  64. TargetRules []string
  65. viperRuleAllowlist `mapstructure:",squash"`
  66. }
  67. // Config is a configuration struct that contains rules and an allowlist if present.
  68. type Config struct {
  69. Title string
  70. Extend Extend
  71. Path string
  72. Description string
  73. Rules map[string]Rule
  74. Keywords map[string]struct{}
  75. // used to keep sarif results consistent
  76. OrderedRules []string
  77. Allowlists []*Allowlist
  78. }
  79. // Extend is a struct that allows users to define how they want their
  80. // configuration extended by other configuration files.
  81. type Extend struct {
  82. Path string
  83. URL string
  84. UseDefault bool
  85. DisabledRules []string
  86. }
  87. func (vc *ViperConfig) Translate() (Config, error) {
  88. var (
  89. keywords = make(map[string]struct{})
  90. orderedRules []string
  91. rulesMap = make(map[string]Rule)
  92. ruleAllowlists = make(map[string][]*Allowlist)
  93. )
  94. // Validate individual rules.
  95. for _, vr := range vc.Rules {
  96. var (
  97. pathPat *regexp.Regexp
  98. regexPat *regexp.Regexp
  99. )
  100. if vr.Path != "" {
  101. pathPat = regexp.MustCompile(vr.Path)
  102. }
  103. if vr.Regex != "" {
  104. regexPat = regexp.MustCompile(vr.Regex)
  105. }
  106. if vr.Keywords == nil {
  107. vr.Keywords = []string{}
  108. } else {
  109. for i, k := range vr.Keywords {
  110. keyword := strings.ToLower(k)
  111. keywords[keyword] = struct{}{}
  112. vr.Keywords[i] = keyword
  113. }
  114. }
  115. if vr.Tags == nil {
  116. vr.Tags = []string{}
  117. }
  118. cr := Rule{
  119. RuleID: vr.ID,
  120. Description: vr.Description,
  121. Regex: regexPat,
  122. SecretGroup: vr.SecretGroup,
  123. Entropy: vr.Entropy,
  124. Path: pathPat,
  125. Keywords: vr.Keywords,
  126. Tags: vr.Tags,
  127. SkipReport: vr.SkipReport,
  128. SmartFilter: vr.SmartFilter,
  129. }
  130. // Parse the rule allowlists, including the older format for backwards compatibility.
  131. if vr.AllowList != nil {
  132. // TODO: Remove this in v9.
  133. if len(vr.Allowlists) > 0 {
  134. return Config{}, fmt.Errorf("%s: [rules.allowlist] is deprecated, it cannot be used alongside [[rules.allowlist]]", cr.RuleID)
  135. }
  136. vr.Allowlists = append(vr.Allowlists, vr.AllowList)
  137. }
  138. for _, a := range vr.Allowlists {
  139. allowlist, err := vc.parseAllowlist(a)
  140. if err != nil {
  141. return Config{}, fmt.Errorf("%s: [[rules.allowlists]] %w", cr.RuleID, err)
  142. }
  143. cr.Allowlists = append(cr.Allowlists, allowlist)
  144. }
  145. for _, r := range vr.Required {
  146. if r.ID == "" {
  147. return Config{}, fmt.Errorf("%s: [[rules.required]] rule ID is empty", cr.RuleID)
  148. }
  149. requiredRule := Required{
  150. RuleID: r.ID,
  151. WithinLines: r.WithinLines,
  152. WithinColumns: r.WithinColumns,
  153. // Distance: r.Distance,
  154. }
  155. cr.RequiredRules = append(cr.RequiredRules, &requiredRule)
  156. }
  157. orderedRules = append(orderedRules, cr.RuleID)
  158. rulesMap[cr.RuleID] = cr
  159. }
  160. // after all the rules have been processed, let's ensure the required rules
  161. // actually exist.
  162. for _, r := range rulesMap {
  163. for _, rr := range r.RequiredRules {
  164. if _, ok := rulesMap[rr.RuleID]; !ok {
  165. return Config{}, fmt.Errorf("%s: [[rules.required]] rule ID '%s' does not exist", r.RuleID, rr.RuleID)
  166. }
  167. }
  168. }
  169. // Assemble the config.
  170. c := Config{
  171. Title: vc.Title,
  172. Description: vc.Description,
  173. Extend: vc.Extend,
  174. Rules: rulesMap,
  175. Keywords: keywords,
  176. OrderedRules: orderedRules,
  177. }
  178. // Parse the config allowlists, including the older format for backwards compatibility.
  179. if vc.AllowList != nil {
  180. // TODO: Remove this in v9.
  181. if len(vc.Allowlists) > 0 {
  182. return Config{}, errors.New("[allowlist] is deprecated, it cannot be used alongside [[allowlists]]")
  183. }
  184. vc.Allowlists = append(vc.Allowlists, vc.AllowList)
  185. }
  186. for _, a := range vc.Allowlists {
  187. allowlist, err := vc.parseAllowlist(&a.viperRuleAllowlist)
  188. if err != nil {
  189. return Config{}, fmt.Errorf("[[allowlists]] %w", err)
  190. }
  191. // Allowlists with |targetRules| aren't added to the global list.
  192. if len(a.TargetRules) > 0 {
  193. for _, ruleID := range a.TargetRules {
  194. // It's not possible to validate |ruleID| until after extend.
  195. ruleAllowlists[ruleID] = append(ruleAllowlists[ruleID], allowlist)
  196. }
  197. } else {
  198. c.Allowlists = append(c.Allowlists, allowlist)
  199. }
  200. }
  201. if maxExtendDepth != extendDepth {
  202. // disallow both usedefault and path from being set
  203. if c.Extend.Path != "" && c.Extend.UseDefault {
  204. return Config{}, errors.New("unable to load config due to extend.path and extend.useDefault being set")
  205. }
  206. if c.Extend.UseDefault {
  207. if err := c.extendDefault(vc); err != nil {
  208. return Config{}, err
  209. }
  210. } else if c.Extend.Path != "" {
  211. if err := c.extendPath(vc); err != nil {
  212. return Config{}, err
  213. }
  214. }
  215. }
  216. // Validate the rules after everything has been assembled (including extended configs).
  217. if extendDepth == 0 {
  218. for _, rule := range c.Rules {
  219. if err := rule.Validate(); err != nil {
  220. return Config{}, err
  221. }
  222. }
  223. // Populate targeted configs.
  224. for ruleID, allowlists := range ruleAllowlists {
  225. rule, ok := c.Rules[ruleID]
  226. if !ok {
  227. return Config{}, fmt.Errorf("[[allowlists]] target rule ID '%s' does not exist", ruleID)
  228. }
  229. rule.Allowlists = append(rule.Allowlists, allowlists...)
  230. c.Rules[ruleID] = rule
  231. }
  232. }
  233. return c, nil
  234. }
  235. func (vc *ViperConfig) parseAllowlist(a *viperRuleAllowlist) (*Allowlist, error) {
  236. var matchCondition AllowlistMatchCondition
  237. switch strings.ToUpper(a.Condition) {
  238. case "AND", "&&":
  239. matchCondition = AllowlistMatchAnd
  240. case "", "OR", "||":
  241. matchCondition = AllowlistMatchOr
  242. default:
  243. return nil, fmt.Errorf("unknown allowlist |condition| '%s' (expected 'and', 'or')", a.Condition)
  244. }
  245. // Validate the target.
  246. regexTarget := a.RegexTarget
  247. if regexTarget != "" {
  248. switch regexTarget {
  249. case "secret":
  250. regexTarget = ""
  251. case "match", "line":
  252. // do nothing
  253. default:
  254. return nil, fmt.Errorf("unknown allowlist |regexTarget| '%s' (expected 'match', 'line')", regexTarget)
  255. }
  256. }
  257. var allowlistRegexes []*regexp.Regexp
  258. for _, a := range a.Regexes {
  259. allowlistRegexes = append(allowlistRegexes, regexp.MustCompile(a))
  260. }
  261. var allowlistPaths []*regexp.Regexp
  262. for _, a := range a.Paths {
  263. allowlistPaths = append(allowlistPaths, regexp.MustCompile(a))
  264. }
  265. allowlist := &Allowlist{
  266. Description: a.Description,
  267. MatchCondition: matchCondition,
  268. Commits: a.Commits,
  269. Paths: allowlistPaths,
  270. RegexTarget: regexTarget,
  271. Regexes: allowlistRegexes,
  272. StopWords: a.StopWords,
  273. }
  274. if err := allowlist.Validate(); err != nil {
  275. return nil, err
  276. }
  277. return allowlist, nil
  278. }
  279. func (c *Config) GetOrderedRules() []Rule {
  280. var orderedRules []Rule
  281. for _, id := range c.OrderedRules {
  282. if _, ok := c.Rules[id]; ok {
  283. orderedRules = append(orderedRules, c.Rules[id])
  284. }
  285. }
  286. return orderedRules
  287. }
  288. func (c *Config) extendDefault(parent *ViperConfig) error {
  289. extendDepth++
  290. viper.SetConfigType("toml")
  291. if err := viper.ReadConfig(strings.NewReader(DefaultConfig)); err != nil {
  292. return fmt.Errorf("failed to load extended default config, err: %w", err)
  293. }
  294. defaultViperConfig := ViperConfig{}
  295. if err := viper.Unmarshal(&defaultViperConfig); err != nil {
  296. return fmt.Errorf("failed to load extended default config, err: %w", err)
  297. }
  298. cfg, err := defaultViperConfig.Translate()
  299. if err != nil {
  300. return fmt.Errorf("failed to load extended default config, err: %w", err)
  301. }
  302. logging.Debug().Msg("extending config with default config")
  303. c.extend(cfg)
  304. return nil
  305. }
  306. func (c *Config) extendPath(parent *ViperConfig) error {
  307. extendDepth++
  308. viper.SetConfigFile(c.Extend.Path)
  309. if err := viper.ReadInConfig(); err != nil {
  310. return fmt.Errorf("failed to load extended config, err: %w", err)
  311. }
  312. extensionViperConfig := ViperConfig{}
  313. if err := viper.Unmarshal(&extensionViperConfig); err != nil {
  314. return fmt.Errorf("failed to load extended config, err: %w", err)
  315. }
  316. cfg, err := extensionViperConfig.Translate()
  317. if err != nil {
  318. return fmt.Errorf("failed to load extended config, err: %w", err)
  319. }
  320. logging.Debug().Msgf("extending config with %s", c.Extend.Path)
  321. c.extend(cfg)
  322. return nil
  323. }
  324. func (c *Config) extendURL() {
  325. // TODO
  326. }
  327. func (c *Config) extend(extensionConfig Config) {
  328. // Get config name for helpful log messages.
  329. var configName string
  330. if c.Extend.Path != "" {
  331. configName = c.Extend.Path
  332. } else {
  333. configName = "default"
  334. }
  335. // Convert |Config.DisabledRules| into a map for ease of access.
  336. disabledRuleIDs := map[string]struct{}{}
  337. for _, id := range c.Extend.DisabledRules {
  338. if _, ok := extensionConfig.Rules[id]; !ok {
  339. logging.Warn().
  340. Str("rule-id", id).
  341. Str("config", configName).
  342. Msg("Disabled rule doesn't exist in extended config.")
  343. }
  344. disabledRuleIDs[id] = struct{}{}
  345. }
  346. for ruleID, baseRule := range extensionConfig.Rules {
  347. // Skip the rule.
  348. if _, ok := disabledRuleIDs[ruleID]; ok {
  349. logging.Debug().
  350. Str("rule-id", ruleID).
  351. Str("config", configName).
  352. Msg("Ignoring rule from extended config.")
  353. continue
  354. }
  355. currentRule, ok := c.Rules[ruleID]
  356. if !ok {
  357. // Rule doesn't exist, add it to the config.
  358. c.Rules[ruleID] = baseRule
  359. for _, k := range baseRule.Keywords {
  360. c.Keywords[k] = struct{}{}
  361. }
  362. c.OrderedRules = append(c.OrderedRules, ruleID)
  363. } else {
  364. // Rule exists, merge our changes into the base.
  365. if currentRule.Description != "" {
  366. baseRule.Description = currentRule.Description
  367. }
  368. if currentRule.Entropy != 0 {
  369. baseRule.Entropy = currentRule.Entropy
  370. }
  371. if currentRule.SecretGroup != 0 {
  372. baseRule.SecretGroup = currentRule.SecretGroup
  373. }
  374. if currentRule.Regex != nil {
  375. baseRule.Regex = currentRule.Regex
  376. }
  377. if currentRule.Path != nil {
  378. baseRule.Path = currentRule.Path
  379. }
  380. baseRule.Tags = append(baseRule.Tags, currentRule.Tags...)
  381. baseRule.Keywords = append(baseRule.Keywords, currentRule.Keywords...)
  382. baseRule.Allowlists = append(baseRule.Allowlists, currentRule.Allowlists...)
  383. // The keywords from the base rule and the extended rule must be merged into the global keywords list
  384. for _, k := range baseRule.Keywords {
  385. c.Keywords[k] = struct{}{}
  386. }
  387. c.Rules[ruleID] = baseRule
  388. }
  389. }
  390. // append allowlists, not attempting to merge
  391. c.Allowlists = append(c.Allowlists, extensionConfig.Allowlists...)
  392. // sort to keep extended rules in order
  393. sort.Strings(c.OrderedRules)
  394. }