deepsecrets 2.1.1__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/PKG-INFO +12 -4
  2. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/README.md +10 -1
  3. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/cli.py +194 -124
  4. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/config.py +39 -7
  5. deepsecrets-2.2.0/deepsecrets/core/engines/regex.py +88 -0
  6. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/engines/semantic.py +14 -2
  7. deepsecrets-2.2.0/deepsecrets/core/helpers/confidence.py +18 -0
  8. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/helpers/content_analyzer.py +2 -2
  9. deepsecrets-2.2.0/deepsecrets/core/helpers/encoding.py +151 -0
  10. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/helpers/naturalness_scorer.py +17 -10
  11. deepsecrets-2.2.0/deepsecrets/core/helpers/regex_candidate_evaluator.py +72 -0
  12. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/helpers/variable_evaluator.py +27 -13
  13. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/model/finding.py +15 -3
  14. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/model/internal/processing.py +10 -0
  15. deepsecrets-2.2.0/deepsecrets/core/model/regex_candidate.py +95 -0
  16. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/model/response/dojo_sarif.py +35 -44
  17. deepsecrets-2.2.0/deepsecrets/core/model/rules/regex.py +195 -0
  18. deepsecrets-2.2.0/deepsecrets/core/model/rules/regex_candidate_scoring.py +142 -0
  19. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/model/rules/rule.py +5 -2
  20. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/model/rules/variable_scoring.py +8 -0
  21. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/modes/iscan_mode.py +221 -28
  22. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/rulesets/ibuilder.py +1 -0
  23. deepsecrets-2.2.0/deepsecrets/core/rulesets/regex_candidate_scoring.py +7 -0
  24. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/tokenizers/cheap_var_search.py +10 -1
  25. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/tokenizers/helpers/semantic/deep_analyzer.py +36 -5
  26. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/tokenizers/helpers/semantic/var_detection/detector.py +3 -0
  27. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/tokenizers/helpers/semantic/var_detection/rules.py +46 -4
  28. deepsecrets-2.2.0/deepsecrets/core/tokenizers/helpers/single_token_improver.py +88 -0
  29. deepsecrets-2.2.0/deepsecrets/core/tokenizers/helpers/token_table.py +99 -0
  30. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/tokenizers/helpers/type_stream.py +49 -1
  31. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/tokenizers/lexer.py +37 -28
  32. deepsecrets-2.2.0/deepsecrets/core/utils/environment.py +37 -0
  33. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/utils/file_analyzer.py +16 -4
  34. deepsecrets-2.2.0/deepsecrets/core/utils/gc_freeze.py +7 -0
  35. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/utils/lexer_finder.py +4 -6
  36. deepsecrets-2.2.0/deepsecrets/core/utils/multiprocessing_setup.py +142 -0
  37. deepsecrets-2.2.0/deepsecrets/rules/excluded_bundles.json +5 -0
  38. deepsecrets-2.2.0/deepsecrets/rules/regex_candidate_scoring_rules.json +180 -0
  39. deepsecrets-2.2.0/deepsecrets/rules/regexes.json +571 -0
  40. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/rules/variable_scoring_rules.json +8 -0
  41. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/scan_modes/cli.py +36 -7
  42. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/pyproject.toml +6 -3
  43. deepsecrets-2.1.1/deepsecrets/core/engines/regex.py +0 -39
  44. deepsecrets-2.1.1/deepsecrets/core/model/rules/regex.py +0 -118
  45. deepsecrets-2.1.1/deepsecrets/core/tokenizers/helpers/single_token_improver.py +0 -104
  46. deepsecrets-2.1.1/deepsecrets/diagnostics/__init__.py +0 -5
  47. deepsecrets-2.1.1/deepsecrets/diagnostics/__main__.py +0 -62
  48. deepsecrets-2.1.1/deepsecrets/diagnostics/trace.py +0 -405
  49. deepsecrets-2.1.1/deepsecrets/diagnostics/worker.py +0 -39
  50. deepsecrets-2.1.1/deepsecrets/rules/regexes.json +0 -237
  51. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/LICENSE +0 -0
  52. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/__init__.py +0 -0
  53. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/__main__.py +0 -0
  54. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/engines/__init__.py +0 -0
  55. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/engines/hashed_secret.py +0 -0
  56. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/engines/iengine.py +0 -0
  57. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/helpers/entropy.py +0 -0
  58. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/model/__init__.py +0 -0
  59. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/model/file.py +0 -0
  60. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/model/response/base.py +0 -0
  61. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/model/response/builtin.py +0 -0
  62. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/model/rules/__init__.py +0 -0
  63. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/model/rules/exlcuded_path.py +0 -0
  64. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/model/rules/false_finding.py +0 -0
  65. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/model/rules/hashed_secret.py +0 -0
  66. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/model/rules/hashing.py +0 -0
  67. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/model/semantic.py +0 -0
  68. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/model/token.py +0 -0
  69. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/model/tokenized_region.py +0 -0
  70. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/rulesets/excluded_paths.py +0 -0
  71. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/rulesets/false_findings.py +0 -0
  72. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/rulesets/hashed_secrets.py +0 -0
  73. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/rulesets/regex.py +0 -0
  74. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/rulesets/variable_scoring.py +0 -0
  75. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/tokenizers/__init__.py +0 -0
  76. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/tokenizers/full_content.py +0 -0
  77. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/tokenizers/helpers/__init__.py +0 -0
  78. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/tokenizers/helpers/cheap_var_search/__init__.py +0 -0
  79. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/tokenizers/helpers/semantic/__init__.py +0 -0
  80. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/tokenizers/helpers/semantic/language.py +0 -0
  81. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/tokenizers/helpers/semantic/var_detection/__init__.py +0 -0
  82. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/tokenizers/helpers/subfile_regions_helper.py +0 -0
  83. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/tokenizers/itokenizer.py +0 -0
  84. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/tokenizers/per_line.py +0 -0
  85. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/tokenizers/per_word.py +0 -0
  86. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/ui/floating_header.py +0 -0
  87. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/ui/progress_bar.py +0 -0
  88. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/ui/time_remaining_column.py +0 -0
  89. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/utils/__init__.py +0 -0
  90. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/utils/cpu.py +0 -0
  91. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/utils/exceptions.py +0 -0
  92. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/utils/finding_merger.py +0 -0
  93. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/utils/fs.py +0 -0
  94. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/utils/guess_filetype.py +0 -0
  95. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/utils/hashing.py +0 -0
  96. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/utils/lifecycle_hooks.py +0 -0
  97. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/utils/log.py +0 -0
  98. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/utils/progress.py +0 -0
  99. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/core/utils/string.py +0 -0
  100. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/rules/excluded_paths.json +0 -0
  101. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/rules/value_scoring_model.json +0 -0
  102. {deepsecrets-2.1.1 → deepsecrets-2.2.0}/deepsecrets/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepsecrets
3
- Version: 2.1.1
3
+ Version: 2.2.0
4
4
  Summary: A better tool for secrets scanning
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -9,7 +9,7 @@ Author: Nikolai Khechumov
9
9
  Author-email: khechumov@gmail.com
10
10
  Maintainer: Nikolai Khechumov
11
11
  Maintainer-email: khechumov@gmail.com
12
- Requires-Python: >=3.10,<4.0.0
12
+ Requires-Python: >=3.11,<4.0.0
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Classifier: License :: OSI Approved :: MIT License
15
15
  Classifier: Operating System :: OS Independent
@@ -17,7 +17,6 @@ Classifier: Environment :: Console
17
17
  Classifier: Topic :: Security
18
18
  Requires-Dist: aenum (==3.1.17)
19
19
  Requires-Dist: jschema-to-python (==1.2.3)
20
- Requires-Dist: jsx-lexer (==2.0.1)
21
20
  Requires-Dist: mmh3 (==5.3.0)
22
21
  Requires-Dist: ordered-set (==4.1.0)
23
22
  Requires-Dist: puppetparser (==0.2.14)
@@ -79,6 +78,8 @@ From PyPi
79
78
  $ pip install deepsecrets
80
79
  ```
81
80
 
81
+ DeepSecrets needs Python 3.11 or later.
82
+
82
83
 
83
84
  ## Scanning
84
85
  The easiest way to run a scan:
@@ -98,7 +99,14 @@ The `--help` command is always ready to guide you, but here are the key flags yo
98
99
  * `--regex-rules /path/to/rules.json`: Supply your own custom regex ruleset.
99
100
  * `--hashed-values /path/to/hashes.json`: Provide a list of pre-hashed known production secrets to search for them securely.
100
101
  * `--excluded-paths /path/to/exclusions.json`: Override or extend the default paths ignored during scanning.
102
+ * `--deep-max-size BYTES`: Files larger than this (default 250,000 bytes) get the fast analysis: the regex rules and the variable search, without the language lexer. Big files are mostly generated or minified, the lexer costs most of the scan time on them and rarely finds more there. `0` gives every file the full analysis.
103
+ * `--skip-bundles`: Skip minified JavaScript, source maps and bundles (`*.min.js`, `*.map`, `*.bundle.js`) entirely, on top of the other path exclusions.
104
+ * `--confidence-level {all,low,medium,high,very-high}`: The lowest confidence to report (default `low`, every finding). `medium`, `high` and `very-high` keep confidence 3, 6 and 9 or more; `all` also reports the regex matches the scanner judged not to be secrets (placeholders, examples), with an `-INFO` rule id.
105
+ * `--report-diagnostics`: Add every file found under the target to the SARIF report, with its status, scan time, skip reason and analysis depth.
106
+ * `--regex-candidate-scoring-rules /path/to/rules.json`: Supply your own rules for judging regex matches before they are reported. Give the flag no value to report regex matches unjudged.
107
+ * `--multiprocessing-context {forkserver,spawn,fork}`: How worker processes start. The default is `forkserver` where the platform has it (Linux, macOS): workers share the scanner already loaded in a server process. Windows uses `spawn`. All three give the same findings.
101
108
  * `--disable-masking`: Keep potential secrets unmasked in the output report *(see caution below)*.
109
+ * `--ci` / `--no-ci`: Turn CI mode on or off. CI mode prints a plain progress line every 30 seconds instead of the live progress display. It turns on by itself inside CI services (GitHub Actions, GitLab CI, Jenkins and others) and whenever the output is not a terminal.
102
110
 
103
111
 
104
112
  ### Github Actions Integration
@@ -147,7 +155,7 @@ Masking doesn't break the deduplication logic of downstream platforms (like Gith
147
155
 
148
156
  Every finding gets a confidence score. However, different security platforms parse SARIF metrics differently. To ensure compatibility across modern ASPM dashboards, DeepSecrets does the following:
149
157
 
150
- * **Virtual Subrules (`rules[]`)**: Dynamically generates rules like `S105-LOW` or `S105-CRITICAL`. This forces GitHub Security and DefectDojo to map semantic precision variance properly without breaking native parsers.
158
+ * **Virtual Subrules (`rules[]`)**: Dynamically generates rules by confidence tier, from `S105-LOW` through `-MEDIUM` and `-HIGH` to `S105-VERY-HIGH` (and `-INFO` for the rejected matches `--confidence-level all` reports). This forces GitHub Security and DefectDojo to map semantic precision variance properly without breaking native parsers.
151
159
 
152
160
  * **Deterministic Result Level**: The tool always explicitly sets `level: error` in the `results[]` model. This acts as a universal fallback for CI/CD pipelines and older SAST parsers, ensuring that exposed secrets reliably break builds or block Pull Requests regardless of individual rule interpretations.
153
161
 
@@ -45,6 +45,8 @@ From PyPi
45
45
  $ pip install deepsecrets
46
46
  ```
47
47
 
48
+ DeepSecrets needs Python 3.11 or later.
49
+
48
50
 
49
51
  ## Scanning
50
52
  The easiest way to run a scan:
@@ -64,7 +66,14 @@ The `--help` command is always ready to guide you, but here are the key flags yo
64
66
  * `--regex-rules /path/to/rules.json`: Supply your own custom regex ruleset.
65
67
  * `--hashed-values /path/to/hashes.json`: Provide a list of pre-hashed known production secrets to search for them securely.
66
68
  * `--excluded-paths /path/to/exclusions.json`: Override or extend the default paths ignored during scanning.
69
+ * `--deep-max-size BYTES`: Files larger than this (default 250,000 bytes) get the fast analysis: the regex rules and the variable search, without the language lexer. Big files are mostly generated or minified, the lexer costs most of the scan time on them and rarely finds more there. `0` gives every file the full analysis.
70
+ * `--skip-bundles`: Skip minified JavaScript, source maps and bundles (`*.min.js`, `*.map`, `*.bundle.js`) entirely, on top of the other path exclusions.
71
+ * `--confidence-level {all,low,medium,high,very-high}`: The lowest confidence to report (default `low`, every finding). `medium`, `high` and `very-high` keep confidence 3, 6 and 9 or more; `all` also reports the regex matches the scanner judged not to be secrets (placeholders, examples), with an `-INFO` rule id.
72
+ * `--report-diagnostics`: Add every file found under the target to the SARIF report, with its status, scan time, skip reason and analysis depth.
73
+ * `--regex-candidate-scoring-rules /path/to/rules.json`: Supply your own rules for judging regex matches before they are reported. Give the flag no value to report regex matches unjudged.
74
+ * `--multiprocessing-context {forkserver,spawn,fork}`: How worker processes start. The default is `forkserver` where the platform has it (Linux, macOS): workers share the scanner already loaded in a server process. Windows uses `spawn`. All three give the same findings.
67
75
  * `--disable-masking`: Keep potential secrets unmasked in the output report *(see caution below)*.
76
+ * `--ci` / `--no-ci`: Turn CI mode on or off. CI mode prints a plain progress line every 30 seconds instead of the live progress display. It turns on by itself inside CI services (GitHub Actions, GitLab CI, Jenkins and others) and whenever the output is not a terminal.
68
77
 
69
78
 
70
79
  ### Github Actions Integration
@@ -113,7 +122,7 @@ Masking doesn't break the deduplication logic of downstream platforms (like Gith
113
122
 
114
123
  Every finding gets a confidence score. However, different security platforms parse SARIF metrics differently. To ensure compatibility across modern ASPM dashboards, DeepSecrets does the following:
115
124
 
116
- * **Virtual Subrules (`rules[]`)**: Dynamically generates rules like `S105-LOW` or `S105-CRITICAL`. This forces GitHub Security and DefectDojo to map semantic precision variance properly without breaking native parsers.
125
+ * **Virtual Subrules (`rules[]`)**: Dynamically generates rules by confidence tier, from `S105-LOW` through `-MEDIUM` and `-HIGH` to `S105-VERY-HIGH` (and `-INFO` for the rejected matches `--confidence-level all` reports). This forces GitHub Security and DefectDojo to map semantic precision variance properly without breaking native parsers.
117
126
 
118
127
  * **Deterministic Result Level**: The tool always explicitly sets `level: error` in the `results[]` model. This acts as a universal fallback for CI/CD pipelines and older SAST parsers, ensuring that exposed secrets reliably break builds or block Pull Requests regardless of individual rule interpretations.
119
128
 
@@ -1,27 +1,39 @@
1
1
  import argparse
2
2
  from datetime import datetime
3
- import json
3
+ import os
4
4
  from argparse import RawTextHelpFormatter
5
5
  from typing import Dict, List
6
6
  from jschema_to_python.to_json import to_json
7
+ from multiprocessing import get_all_start_methods
7
8
 
8
9
  from deepsecrets import MODULE_NAME, console
9
- from deepsecrets.config import SCANNER_VERSION, SCANNER_VERSION_NUMERIC, Config, config, Output
10
+ from deepsecrets.config import (
11
+ CONFIDENCE_LEVELS,
12
+ DEFAULT_CONFIDENCE_LEVEL,
13
+ DEFAULT_DEEP_MAX_SIZE,
14
+ SCANNER_VERSION,
15
+ Config,
16
+ Output,
17
+ )
10
18
  from deepsecrets.core.engines.hashed_secret import HashedSecretEngine
11
19
  from deepsecrets.core.engines.regex import RegexEngine
12
20
  from deepsecrets.core.engines.semantic import SemanticEngine
13
21
  from deepsecrets.core.model.finding import Finding
14
- from deepsecrets.core.model.response.builtin import BuiltinFormatResponseBuilder
15
22
  from deepsecrets.core.model.response.dojo_sarif import DojoSarifResponseBuilder
16
23
  from deepsecrets.core.rulesets.false_findings import FalseFindingsBuilder
17
24
  from deepsecrets.core.rulesets.hashed_secrets import HashedSecretsRulesetBuilder
18
25
  from deepsecrets.core.rulesets.regex import RegexRulesetBuilder
26
+ from deepsecrets.core.rulesets.regex_candidate_scoring import RegexCandidateScoringRulesetBuilder
19
27
  from deepsecrets.core.rulesets.variable_scoring import VariableScoringRulesetBuilder
20
28
  from deepsecrets.core.ui.progress_bar import DSApplicationProgess
21
29
  from deepsecrets.core.ui.time_remaining_column import SyncedTimeRemainingColumn
30
+ from deepsecrets.core.utils.exceptions import FileNotFoundException
22
31
  from deepsecrets.core.utils.fs import get_abspath, get_path_inside_package
23
32
  from deepsecrets.core.utils.log import logger
24
33
  from deepsecrets.scan_modes.cli import CliScanMode
34
+ from deepsecrets.core.modes.iscan_mode import WorkerStartupError
35
+ from deepsecrets.core.utils.environment import is_ci_environment
36
+ from deepsecrets.core.utils.multiprocessing_setup import TempDirTooLongError, default_start_method, stop_forkserver
25
37
 
26
38
  from rich.progress import SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
27
39
  from rich.panel import Panel
@@ -59,6 +71,8 @@ overall_time_column.progress_instance = progress_bar
59
71
 
60
72
  class DeepSecretsCliTool:
61
73
  argparser: argparse.ArgumentParser
74
+ # built by parse_arguments, one per parse
75
+ config: Config
62
76
 
63
77
  def __init__(self, args: List[str]):
64
78
  self.args = args
@@ -67,9 +81,6 @@ class DeepSecretsCliTool:
67
81
  def say_hello(self) -> None:
68
82
  console.line(2)
69
83
  console.rule('DeepSecrets', characters='=')
70
-
71
- from rich.panel import Panel
72
-
73
84
  console.print(
74
85
  Align(
75
86
  Panel.fit(
@@ -109,11 +120,25 @@ class DeepSecretsCliTool:
109
120
  default=['built-in'],
110
121
  )
111
122
 
123
+ parser.add_argument(
124
+ '--regex-candidate-scoring-rules',
125
+ nargs='*',
126
+ type=str,
127
+ help='Controls rules for judging regex matches (candidates) before they are reported: placeholders,\n'
128
+ 'language-like values, hosts, test paths. Used by regex rules that declare an "evidence" class.\n'
129
+ '- Ignore this argument to use the built-in ruleset\n'
130
+ "- Using your own rulesets disables the default one. Add 'built-in' to the args list to merge rulesets\n"
131
+ 'eq. --regex-candidate-scoring-rules built-in /root/my_candidate_rules.json\n'
132
+ '- Give it no value to report regex matches unjudged, as before 2.2\n',
133
+ default=['built-in'],
134
+ )
135
+
112
136
  parser.add_argument(
113
137
  '--hashed-values',
114
138
  nargs='*',
115
139
  type=str,
116
- help='Path to your Hashed Values set.\n' "Don't set any value to disable this checks\n",
140
+ help='Path to your Hashed Values set.\n'
141
+ 'Leave the flag out, or give it no value, to disable these checks\n',
117
142
  )
118
143
 
119
144
  parser.add_argument(
@@ -150,6 +175,34 @@ class DeepSecretsCliTool:
150
175
  default=['built-in'],
151
176
  )
152
177
 
178
+ parser.add_argument(
179
+ '--skip-bundles',
180
+ action='store_true',
181
+ help='Also skip minified JavaScript, source maps and bundles (*.min.js, *.map, *.bundle.js), whatever\n'
182
+ "--excluded-paths lists (except 'disable'). Most of them are larger than --deep-max-size and get the\n"
183
+ 'fast analysis anyway.\n',
184
+ )
185
+
186
+ parser.add_argument(
187
+ '--deep-max-size',
188
+ type=int,
189
+ default=DEFAULT_DEEP_MAX_SIZE,
190
+ help='Files larger than this many bytes get the fast analysis: the regex rules and the variable search,\n'
191
+ 'without the language lexer, which costs most of the scan time on big files and rarely finds more there.\n'
192
+ f'Default: {DEFAULT_DEEP_MAX_SIZE}. 0 gives every file the full analysis.\n',
193
+ )
194
+
195
+ parser.add_argument(
196
+ '--confidence-level',
197
+ type=str,
198
+ default=DEFAULT_CONFIDENCE_LEVEL,
199
+ choices=list(CONFIDENCE_LEVELS),
200
+ help='The lowest confidence to report, from all to very-high (default: low).\n'
201
+ '"low" reports every finding; "medium", "high" and "very-high" keep confidence 3, 6 and 9 or more.\n'
202
+ '"all" also reports the regex matches the scanner judged not to be secrets (placeholders, examples),\n'
203
+ 'at confidence 0 with an -INFO rule id.\n',
204
+ )
205
+
153
206
  parser.add_argument(
154
207
  '--false-findings',
155
208
  nargs='*',
@@ -195,9 +248,20 @@ class DeepSecretsCliTool:
195
248
  parser.add_argument(
196
249
  '--multiprocessing-context',
197
250
  type=str,
198
- default='spawn',
199
- choices=['fork', 'spawn', 'forkserver'],
200
- help='Control the multiprocessing context\n',
251
+ default=None,
252
+ # the platform's own: fork and forkserver do not exist on Windows
253
+ choices=get_all_start_methods(),
254
+ help='How worker processes start. Default: forkserver where available (Linux, macOS), spawn elsewhere.\n'
255
+ 'forkserver workers share the scanner already loaded in the server instead of loading it again.\n',
256
+ )
257
+
258
+ parser.add_argument(
259
+ '--ci',
260
+ action=argparse.BooleanOptionalAction,
261
+ default=None,
262
+ help='CI mode: no live progress bars, a plain progress line every 30 s, and no progress manager process.\n'
263
+ 'Default: on inside a CI service (CI, GITHUB_ACTIONS, GITLAB_CI and similar variables) or when the\n'
264
+ 'output is not a terminal; --no-ci forces the live display.\n',
201
265
  )
202
266
 
203
267
  parser.add_argument('--outfile', required=True, type=str)
@@ -208,7 +272,7 @@ class DeepSecretsCliTool:
208
272
  choices=['json', 'sarif', 'dojo-sarif'],
209
273
  help='"sarif": SARIF format (specification accurate, default)\n'
210
274
  '"dojo-sarif": SARIF format (compatible with DefectDojo\'s parser)\n'
211
- '"json": old internal format (deprecated and will be removed soon)',
275
+ '"json": the old internal format, removed in 2.2.0 (exits with an error)',
212
276
  )
213
277
 
214
278
  parser.add_argument(
@@ -222,7 +286,8 @@ class DeepSecretsCliTool:
222
286
  '--report-diagnostics',
223
287
  action='store_true',
224
288
  help='Add scan diagnostics to a SARIF report: every file found under the target directory in\n'
225
- 'run.artifacts (scan time in ms, status, skip reason) and per-file errors in run.invocations.\n',
289
+ 'run.artifacts (scan time in ms, status, skip reason, analysis depth) and per-file errors in\n'
290
+ 'run.invocations.\n',
226
291
  )
227
292
 
228
293
  parser.add_argument('--benchmarking-mode', help=argparse.SUPPRESS, action='store_true')
@@ -231,17 +296,16 @@ class DeepSecretsCliTool:
231
296
  self.argparser = parser
232
297
 
233
298
  def parse_arguments(self) -> None:
299
+ # a fresh config for every parse: nothing an earlier run set carries over (KI-CLI-12)
300
+ config = self.config = Config()
234
301
 
235
302
  user_args = self.argparser.parse_args(args=self.args[1:])
236
- if user_args.verbose:
237
- pass
238
- # config.set_logging_level(logging.DEBUG)
239
303
 
240
304
  if user_args.disable_masking:
241
305
  config.set_disable_masking(True)
242
306
 
243
- # set on every parse: the config singleton outlives one run
244
307
  config.set_report_diagnostics(user_args.report_diagnostics)
308
+ config.set_confidence_level(user_args.confidence_level)
245
309
 
246
310
  if user_args.benchmarking_mode:
247
311
  config._set_benchmarking_mode(True)
@@ -251,10 +315,16 @@ class DeepSecretsCliTool:
251
315
  config.set_workdir(user_args.target_dir)
252
316
  config.set_oneshot_path(user_args.oneshot)
253
317
  config.set_max_file_size(user_args.max_file_size)
318
+ config.set_deep_max_size(user_args.deep_max_size)
254
319
  config.set_process_count(user_args.process_count)
255
- config.set_mp_context(user_args.multiprocessing_context)
320
+ config.set_mp_context(user_args.multiprocessing_context or default_start_method())
321
+ config.set_ci_mode(is_ci_environment() if user_args.ci is None else user_args.ci)
256
322
  config.set_verbose(user_args.verbose)
257
323
  config.output = Output(type=user_args.outformat, path=user_args.outfile)
324
+ # checked now, not when the report is written after the whole scan
325
+ report_dir = os.path.dirname(get_abspath(user_args.outfile))
326
+ if not os.path.isdir(report_dir):
327
+ raise FileNotFoundException(f'--outfile: the directory {report_dir} does not exist')
258
328
 
259
329
  if user_args.reflect_findings_in_return_code:
260
330
  config.return_code_if_findings = True
@@ -262,16 +332,24 @@ class DeepSecretsCliTool:
262
332
  EXCLUDE_PATHS_BUILTIN = get_path_inside_package('rules/excluded_paths.json')
263
333
  if user_args.excluded_paths is not None:
264
334
  rules = [rule.replace('built-in', EXCLUDE_PATHS_BUILTIN) for rule in user_args.excluded_paths]
335
+ if user_args.skip_bundles and 'disable' not in user_args.excluded_paths:
336
+ rules.append(get_path_inside_package('rules/excluded_bundles.json'))
265
337
  config.set_global_exclusion_paths(rules)
266
338
 
267
- config.engines = []
268
-
269
339
  REGEX_BUILTIN_RULESET = get_path_inside_package('rules/regexes.json')
270
340
  if user_args.regex_rules is not None:
271
341
  rules = [rule.replace('built-in', REGEX_BUILTIN_RULESET) for rule in user_args.regex_rules]
272
342
  config.engines.append(RegexEngine)
273
343
  config.add_ruleset(RegexRulesetBuilder, rules)
274
344
 
345
+ CANDIDATE_BUILTIN_RULESET = get_path_inside_package('rules/regex_candidate_scoring_rules.json')
346
+ if user_args.regex_candidate_scoring_rules is not None:
347
+ rules = [
348
+ rule.replace('built-in', CANDIDATE_BUILTIN_RULESET)
349
+ for rule in user_args.regex_candidate_scoring_rules
350
+ ]
351
+ config.add_ruleset(RegexCandidateScoringRulesetBuilder, rules)
352
+
275
353
  conf_semantic_analysis = user_args.semantic_analysis
276
354
  if conf_semantic_analysis is not None and conf_semantic_analysis != DISABLED:
277
355
  config.engines.append(SemanticEngine)
@@ -284,7 +362,9 @@ class DeepSecretsCliTool:
284
362
  config.add_ruleset(VariableScoringRulesetBuilder, rules)
285
363
 
286
364
  conf_hashed_ruleset = user_args.hashed_values
287
- if conf_hashed_ruleset is not None and conf_hashed_ruleset != DISABLED:
365
+ # the flag without a value disables the check, as its help says; an empty ruleset would only keep the lexer on
366
+ # every file for nothing (the hashed engine lexes files over --deep-max-size too)
367
+ if conf_hashed_ruleset and conf_hashed_ruleset != DISABLED:
288
368
  config.engines.append(HashedSecretEngine)
289
369
  config.add_ruleset(HashedSecretsRulesetBuilder, conf_hashed_ruleset)
290
370
 
@@ -293,10 +373,7 @@ class DeepSecretsCliTool:
293
373
  config.add_ruleset(FalseFindingsBuilder, conf_false_findings_ruleset)
294
374
 
295
375
  def get_current_config(self) -> Config:
296
- return config
297
-
298
- def _add_ignorefiles(self, files: List[str]):
299
- config.set_global_exclusion_paths(files)
376
+ return self.config
300
377
 
301
378
  def start(self) -> int: # pragma: nocover
302
379
  startup_time = datetime.now()
@@ -305,52 +382,78 @@ class DeepSecretsCliTool:
305
382
  except Exception as e:
306
383
  logger.exception(e)
307
384
  return ReturnCodes.ERROR
385
+ config = self.config
308
386
 
309
387
  if config.output.type == 'json':
310
- console.print('\n')
311
- if SCANNER_VERSION_NUMERIC[0] == 2 and SCANNER_VERSION_NUMERIC[1] < 2:
312
- console.print(
313
- Align(
314
- Panel(
315
- "The internal JSON report format is now DEPRECATED and will be removed in release 2.2.0\n\nConsider switching now.",
316
- padding=(1, 2),
317
- title=Text('SARIF IS NOW DEFAULT OUTPUT FORMAT', style='reverse'),
318
- highlight=True,
319
- subtitle=Text(' --outformat sarif ', style='reverse'),
320
- title_align='center',
321
- width=90,
322
- subtitle_align='center',
323
- box=box.HEAVY,
324
- style='black on orange_red1',
325
- expand=False,
326
- ),
327
- align='center',
328
- )
329
- )
330
-
331
- else:
332
- console.print(
333
- Align(
334
- Panel(
335
- f"The internal JSON report format was DEPRECATED since the release 2.2.0.\nNow ({SCANNER_VERSION}) it is REMOVED. Switch to SARIF\n.",
336
- padding=(1, 2),
337
- title=Text('SARIF IS NOW DEFAULT OUTPUT FORMAT', style='reverse'),
338
- highlight=True,
339
- subtitle=Text(' --outformat sarif ', style='reverse'),
340
- title_align='center',
341
- width=90,
342
- subtitle_align='center',
343
- box=box.HEAVY,
344
- style='black on orange_red1',
345
- expand=False,
346
- ),
347
- align='center',
348
- )
349
- )
350
- return ReturnCodes.ERROR
351
-
352
- console.print('\n\n')
388
+ self._refuse_json_output()
389
+ return ReturnCodes.ERROR
390
+
391
+ self._print_plan()
392
+
393
+ try:
394
+ mode = CliScanMode(config=config)
395
+ except TempDirTooLongError as e:
396
+ console.print(f'[bold red][!] Cannot start worker processes: {e}.[/bold red]')
397
+ return ReturnCodes.ERROR
398
+
399
+ console.line()
400
+
401
+ if not config.ci_mode:
402
+ # run() starts it once the worker pool exists
403
+ mode.set_progress_bar(progress_bar)
404
+ mode.progress_bar.set_start_time(startup_time)
405
+
406
+ try:
407
+ findings, errors, timings = mode.run()
408
+ except WorkerStartupError as e:
409
+ if mode.progress_bar is not None:
410
+ mode.progress_bar.stop()
411
+ console.print(f'[bold red][!] Scan stopped: {e}. Every file it took would fail the same way.[/bold red]')
412
+ self._release(mode)
413
+ return ReturnCodes.ERROR
414
+
415
+ if mode.progress_bar is not None:
416
+ mode.progress_bar.stop()
417
+ report_path = get_abspath(config.output.path)
418
+ self._print_summary(mode, findings, errors, datetime.now() - startup_time, report_path)
419
+
420
+ if config._benchmarking_mode is True:
421
+ return findings, errors, timings, mode._oneshot_file
422
+
423
+ self._write_report(mode, findings, report_path)
424
+ self._say_goodbye(findings)
425
+ self._release(mode)
426
+
427
+ if len(findings) > 0 and config.return_code_if_findings:
428
+ return ReturnCodes.FINDINGS_DETECTED
429
+
430
+ return ReturnCodes.OK
431
+
432
+ @staticmethod
433
+ def _refuse_json_output() -> None: # pragma: nocover
434
+ console.print('\n')
435
+ console.print(
436
+ Align(
437
+ Panel(
438
+ 'The internal JSON report format was deprecated in 2.1 and is removed in 2.2.0.\n'
439
+ 'Use --outformat sarif (the default) or dojo-sarif.',
440
+ padding=(1, 2),
441
+ title=Text('SARIF IS NOW DEFAULT OUTPUT FORMAT', style='reverse'),
442
+ highlight=True,
443
+ subtitle=Text(' --outformat sarif ', style='reverse'),
444
+ title_align='center',
445
+ width=90,
446
+ subtitle_align='center',
447
+ box=box.HEAVY,
448
+ style='black on orange_red1',
449
+ expand=False,
450
+ ),
451
+ align='center',
452
+ )
453
+ )
353
454
 
455
+ def _print_plan(self) -> None: # pragma: nocover
456
+ config = self.config
354
457
  console.rule(
355
458
  f'Planning a scan against {config.workdir_path} using {config.process_count} process(es)', characters='='
356
459
  )
@@ -371,31 +474,9 @@ class DeepSecretsCliTool:
371
474
  f'[bold yellow]:warning:[/bold yellow] The tool will return code of {ReturnCodes.FINDINGS_DETECTED} if any findings are detected\n'
372
475
  )
373
476
 
374
- mode = CliScanMode(config=config)
375
-
376
- console.line()
377
-
378
- mode.set_progress_bar(progress_bar)
379
- mode.progress_bar.set_start_time(startup_time)
380
- mode.progress_bar.start()
381
-
382
- findings: List[Finding]
383
- errors: Dict[str, List[str]]
384
- timings: Dict[str, int]
385
-
386
- findings, errors, timings = mode.run()
387
-
388
- '''
389
- for finding in findings:
390
- if finding._mapped_on_file is False:
391
- continue
392
- finding.file = None
393
- '''
394
-
395
- mode.progress_bar.stop()
396
- finish_time = datetime.now()
397
- report_path = get_abspath(config.output.path)
398
-
477
+ def _print_summary(
478
+ self, mode: CliScanMode, findings: List[Finding], errors: Dict[str, List[str]], elapsed, report_path: str
479
+ ) -> None: # pragma: nocover
399
480
  console.line()
400
481
  console.print('[bold green]Scanning finished successfully', justify='center')
401
482
  console.line()
@@ -416,7 +497,7 @@ class DeepSecretsCliTool:
416
497
  Align('Processed Files (Tokens)', vertical='middle'),
417
498
  f'{str(len(mode.filepaths))} ({mode.stats.tokens_processed})',
418
499
  )
419
- table.add_row(Align('Elapsed', vertical='middle'), f'{(finish_time-startup_time).total_seconds():.1f}s')
500
+ table.add_row(Align('Elapsed', vertical='middle'), f'{elapsed.total_seconds():.1f}s')
420
501
  errors_line_color = '[bold red]' if len(errors.keys()) > 0 else '[bold green]'
421
502
  table.add_row(
422
503
  Align(f'{errors_line_color}File Errors', vertical='middle'),
@@ -429,36 +510,24 @@ class DeepSecretsCliTool:
429
510
  Align(f'{findings_line_color}Potential Findings', vertical='middle'),
430
511
  f'{findings_line_color}{str(len(findings))}',
431
512
  )
432
- table.add_row(Align(f'Report Location ({config.output.type})', vertical='middle'), report_path)
513
+ table.add_row(Align(f'Report Location ({self.config.output.type})', vertical='middle'), report_path)
433
514
  console.print(Align(table, align='center'))
434
515
 
435
- if config._benchmarking_mode is True:
436
- return findings, errors, timings, mode._oneshot_file
437
-
516
+ def _write_report(self, mode: CliScanMode, findings: List[Finding], report_path: str) -> None:
517
+ # sarif and dojo-sarif are the same document; built before the file is opened, so a failure leaves no
518
+ # truncated report behind
519
+ report = (
520
+ DojoSarifResponseBuilder()
521
+ .with_current_mode(mode)
522
+ .with_findings_list(findings)
523
+ .with_masking_enabled(not self.config.disable_masking)
524
+ .build()
525
+ )
438
526
  with open(report_path, 'w+') as f:
527
+ f.write(to_json(report))
439
528
 
440
- if config.output.type == 'json':
441
- json.dump(
442
- BuiltinFormatResponseBuilder()
443
- .with_current_mode(mode)
444
- .with_findings_list(findings)
445
- .with_masking_enabled(not config.disable_masking)
446
- .build(),
447
- f,
448
- )
449
-
450
- if config.output.type in ['sarif', 'dojo-sarif']:
451
- f.write(
452
- to_json(
453
- DojoSarifResponseBuilder()
454
- .with_current_mode(mode)
455
- .with_findings_list(findings)
456
- .with_masking_enabled(not config.disable_masking)
457
- .build()
458
- )
459
- )
460
-
461
- if len(findings) > 0 and config.disable_masking:
529
+ def _say_goodbye(self, findings: List[Finding]) -> None: # pragma: nocover
530
+ if len(findings) > 0 and self.config.disable_masking:
462
531
  console.print(
463
532
  '[bold red]:warning: SECRETS MASKING WAS DISABLED, THE REPORT CONTAINS POTENTIAL SECRETS IN PLAINTEXT.\nBE CAREFUL!',
464
533
  justify='center',
@@ -473,9 +542,10 @@ class DeepSecretsCliTool:
473
542
  console.line()
474
543
  console.print(Align('[bold green]FINISHED', align='center'))
475
544
  console.line(2)
476
- mode.dispose()
477
545
 
478
- if len(findings) > 0 and config.return_code_if_findings:
479
- return ReturnCodes.FINDINGS_DETECTED
480
-
481
- return ReturnCodes.OK
546
+ @staticmethod
547
+ def _release(mode: CliScanMode) -> None:
548
+ mode.dispose()
549
+ # the pool and the manager are gone: wait for the forkserver, so whoever waits for this process sees the
550
+ # workers' CPU time (KI-CLI-42)
551
+ stop_forkserver()