config.go 13 KB

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