Skip to content

Commit 12507ae

Browse files
authored
Add support for ignore rules (#184)
1 parent 1666d0e commit 12507ae

File tree

10 files changed

+267
-17
lines changed

10 files changed

+267
-17
lines changed

components/resc-vcs-scanner/README.md

+35-4
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ To create vcs_instances_config.json file please refer to: [Structure of vcs_inst
163163
cd components/resc-vcs-scanner
164164
pip install virtualenv
165165
virtualenv venv
166-
source venv/Scripts/activate
166+
source venv/bin/activate
167167
```
168168
#### 2. Install resc_vcs_scanner package:
169169
```bash
@@ -174,19 +174,19 @@ The CLI has 3 modes of operation, please make use of the --help argument to see
174174
- Scanning a non-git directory:
175175
```bash
176176
secret_scanner dir --help
177-
secret_scanner dir --gitleaks-rules-path=<path to gitleaks toml rule> --gitleaks-path=<path to gitleaks binary> --dir=<directory to scan>
177+
secret_scanner dir --gitleaks-rules-path=<path to gitleaks toml rule> --gitleaks-path=<path to gitleaks binary> --ignored-blocker-path=<path to resc-ignore.dsv file> --dir=<directory to scan>
178178
```
179179

180180
- Scanning an already cloned git repository:
181181
```bash
182182
secret_scanner repo local --help
183-
secret_scanner repo local --gitleaks-rules-path=<path to gitleaks toml rule> --gitleaks-path=<path to gitleaks binary> --dir=<directory of repository to scan>
183+
secret_scanner repo local --gitleaks-rules-path=<path to gitleaks toml rule> --gitleaks-path=<path to gitleaks binary> --ignored-blocker-path=<path to resc-ignore.dsv file> --dir=<directory of repository to scan>
184184
```
185185

186186
- Scanning a remote git repository:
187187
```bash
188188
secret_scanner repo remote --help
189-
secret_scanner repo remote --gitleaks-rules-path=<path to gitleaks toml rule> --gitleaks-path=<path to gitleaks binary> --repo-url=<url of repository to scan>
189+
secret_scanner repo remote --gitleaks-rules-path=<path to gitleaks toml rule> --gitleaks-path=<path to gitleaks binary> --ignored-blocker-path=<path to resc-ignore.dsv file> --repo-url=<url of repository to scan>
190190
```
191191
Most CLI arguments can also be provided by setting the corresponding environment variable.
192192
Please see the --help options on the arguments that can be provided using environment variables, and the expected environment variable names.
@@ -195,6 +195,37 @@ These will always be prefixed with RESC_
195195
Example: the argument **--gitleaks-path** can be provided using the environment variable **RESC_GITLEAKS_PATH**
196196
</details>
197197

198+
### Ignoring findings
199+
200+
<details>
201+
<summary>Preview</summary>
202+
203+
It is possible to ignore some blocker findings (e.g. false positive) by providing
204+
a `resc-ignore.dsv` file. The bockers will be downgraded to a warning level and marked as **ignored**. Such file has the following structure:
205+
206+
```sh
207+
# This is a comment
208+
finding_path|finding_rule|finding_line_number|expiration_date
209+
finding_path_2|finding_rule_2|finding_line_number_2
210+
```
211+
212+
- `finding_path` contains the path to the file with the blocking finding.
213+
- `finding_rule` contains the name of the blocking rule.
214+
- `finding_line_number` contains the line number of the finding.
215+
- `expiration_date` is optional, contains the date in ISO 8601 format until which this ignore rule should be considered valid.
216+
217+
For example, if we want to ignore the finding in file `/etc/passwd` for rule `root_value_found` on line `1` until April 1st 2024 at 23:59 the following line should be used.
218+
```sh
219+
/etc/passwd|root_value_found|1|2024-04-01T23:59:00
220+
```
221+
To ignore this finding _ad vitam aeternam_:
222+
```sh
223+
/etc/passwd|root_value_found|1
224+
```
225+
226+
227+
</details>
228+
198229
## Testing
199230
Run below commands to make sure that the unit tests are running and that the code matches quality standards:
200231

components/resc-vcs-scanner/src/vcs_scanner/helpers/finding_action.py

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
class FindingAction(str, Enum):
66
INFO = "Info"
77
WARN = "Warn"
8+
IGNORED = "Ignored"
89
BLOCK = "Block"

components/resc-vcs-scanner/src/vcs_scanner/secret_scanners/cli.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ def create_cli_argparser() -> ArgumentParser:
4747
envvar="RESC_GITLEAKS_RULES_PATH", help="Path to the gitleaks rules file. "
4848
"Can also be set via the "
4949
"RESC_GITLEAKS_RULES_PATH environment variable")
50+
parser_common.add_argument("--ignored-blocker-path", type=pathlib.Path, action=EnvDefault, required=False,
51+
envvar="RESC_IGNORED_BLOCKER_PATH", help="Path to the resc-ignore.dsv file. "
52+
"Can also be set via the "
53+
"RESC_IGNORED_BLOCKER_PATH "
54+
"environment variable")
5055
parser_common.add_argument("-w", "--exit-code-warn", required=False, action=EnvDefault, default=2, type=int,
5156
envvar="RESC_EXIT_CODE_WARN",
5257
help="Exit code given if CLI encounters findings tagged with Warn, default 2. "
@@ -199,8 +204,10 @@ def scan_directory(args: Namespace):
199204
)
200205

201206
output_plugin = STDOUTWriter(toml_rule_file_path=args.gitleaks_rules_path,
202-
exit_code_warn=args.exit_code_warn, exit_code_block=args.exit_code_block,
203-
filter_tag=args.filter_tag)
207+
exit_code_warn=args.exit_code_warn,
208+
exit_code_block=args.exit_code_block,
209+
filter_tag=args.filter_tag,
210+
ignore_findings_path=args.ignored_blocker_path)
204211
with open(args.gitleaks_rules_path, encoding="utf-8") as rule_pack:
205212
rule_pack_version = get_rule_pack_version_from_file(rule_pack.read())
206213
if not rule_pack_version:
@@ -244,8 +251,10 @@ def scan_repository(args: Namespace):
244251

245252
else:
246253
output_plugin = STDOUTWriter(toml_rule_file_path=args.gitleaks_rules_path,
247-
exit_code_warn=args.exit_code_warn, exit_code_block=args.exit_code_block,
248-
filter_tag=args.filter_tag)
254+
exit_code_warn=args.exit_code_warn,
255+
exit_code_block=args.exit_code_block,
256+
filter_tag=args.filter_tag,
257+
ignore_findings_path=args.ignored_blocker_path)
249258
with open(args.gitleaks_rules_path, encoding="utf-8") as rule_pack:
250259
rule_pack_version = get_rule_pack_version_from_file(rule_pack.read())
251260
if not rule_pack_version:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# pylint: disable=E1101
2+
# Standard Library
3+
import csv
4+
import logging
5+
from datetime import datetime
6+
7+
# Third Party
8+
import dateutil.parser
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class IgnoredListProvider(): # pylint: disable=R0902
14+
15+
def __init__(self, ignore_findings_path: str):
16+
self.ignore_findings_path: str = ignore_findings_path
17+
self.today: datetime = datetime.now()
18+
19+
def get_ignore_list(self) -> dict:
20+
"""
21+
Get the dictionary of ignored findings according to the file
22+
The output will contain a dictionary with the findings id as the key and the tags as a list in the value
23+
"""
24+
ignored = {}
25+
26+
try:
27+
# read dsv: `path|rule_name|line_number|expiry_date`
28+
with open(self.ignore_findings_path, encoding="utf-8") as ignore_findings_file:
29+
csv_ignore_list = csv.reader(ignore_findings_file, delimiter='|')
30+
for row in csv_ignore_list:
31+
expire: datetime = datetime.now()
32+
33+
# rows starting with # are comments
34+
if row[0][:1] == '#':
35+
continue
36+
37+
if len(row) < 3:
38+
string_row: str = "".join(row)
39+
logger.warning(f"Skipping: incomplete entry for {string_row}")
40+
continue
41+
42+
if len(row) > 3:
43+
date = row[3]
44+
try:
45+
expire: datetime = dateutil.parser.isoparse(date)
46+
except ValueError:
47+
logger.warning(f"Skipping: invalid date entry for {date}")
48+
continue
49+
50+
if expire < self.today:
51+
continue
52+
53+
# we use the path, rule_name, line_number as a dictionary key
54+
ignored[row[0] + '|' + row[1] + '|' + row[2]] = True
55+
except FileNotFoundError: # <- File does not exists: we just fail silently
56+
logger.warning(f"could not find {self.ignore_findings_path}")
57+
return {}
58+
59+
return ignored

components/resc-vcs-scanner/src/vcs_scanner/secret_scanners/stdout_writer.py

+26-8
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,28 @@
1818
from vcs_scanner.helpers.finding_action import FindingAction
1919
from vcs_scanner.model import VCSInstanceRuntime
2020
from vcs_scanner.output_module import OutputModule
21+
from vcs_scanner.secret_scanners.ignore_list_provider import IgnoredListProvider
2122

2223
logger = logging.getLogger(__name__)
2324

2425

2526
class STDOUTWriter(OutputModule):
2627

27-
def __init__(self, toml_rule_file_path: str, exit_code_warn: int, exit_code_block: int, filter_tag: str = None):
28+
def __init__(self,
29+
toml_rule_file_path: str,
30+
exit_code_warn: int,
31+
exit_code_block: int,
32+
filter_tag: str = None,
33+
ignore_findings_path: str = ""):
2834
self.toml_rule_file_path: str = toml_rule_file_path
2935
self.exit_code_warn: int = exit_code_warn
3036
self.exit_code_block: int = exit_code_block
3137
self.filter_tag: str = filter_tag
3238
self.exit_code_success = 0
39+
self.ignore_findings_providers: IgnoredListProvider = IgnoredListProvider(ignore_findings_path)
3340

34-
def write_vcs_instance(self, vcs_instance_runtime: VCSInstanceRuntime) -> Optional[VCSInstanceRead]:
41+
def write_vcs_instance(self,
42+
vcs_instance_runtime: VCSInstanceRuntime) -> Optional[VCSInstanceRead]:
3543
vcs_instance = VCSInstanceRead(id_=1,
3644
name=vcs_instance_runtime.name,
3745
provider_type=vcs_instance_runtime.provider_type,
@@ -66,13 +74,15 @@ def _get_rule_tags(self) -> dict:
6674
return rule_tags
6775

6876
@staticmethod
69-
def _determine_finding_action(finding: FindingCreate, rule_tags: dict) -> FindingAction:
77+
def _determine_finding_action(finding: FindingCreate, rule_tags: dict, ignore_dictionary: dict) -> FindingAction:
7078
"""
7179
Determine the action to take for the finding, based on the rule tags
7280
:param finding:
7381
FindingCreate instance of the finding
7482
:param rule_tags:
75-
Dictionary continuing all the rules and there respective tags
83+
Dictionary containing all the rules and there respective tags
84+
:param ignore_dictionary:
85+
Dictionary containing all the list of ignored blockers
7686
:return: FindingAction.
7787
FindingAction to take for this finding
7888
"""
@@ -81,6 +91,12 @@ def _determine_finding_action(finding: FindingCreate, rule_tags: dict) -> Findin
8191
rule_action = FindingAction.WARN
8292
if FindingAction.BLOCK in rule_tags.get(finding.rule_name, []):
8393
rule_action = FindingAction.BLOCK
94+
95+
if rule_action == FindingAction.BLOCK:
96+
key: str = finding.file_path + "|" + finding.rule_name + "|" + str(finding.line_number)
97+
if key in ignore_dictionary:
98+
rule_action = FindingAction.IGNORED
99+
84100
return rule_action
85101

86102
def _finding_tag_filter(self, finding: FindingCreate, rule_tags: dict, filter_tag: str) -> bool:
@@ -89,7 +105,7 @@ def _finding_tag_filter(self, finding: FindingCreate, rule_tags: dict, filter_ta
89105
:param finding:
90106
FindingCreate instance of the finding
91107
:param rule_tags:
92-
Dictionary continuing all the rules and there respective tags
108+
Dictionary containing all the rules and there respective tags
93109
:Param: filter_tag.
94110
filter_tag will check for the tag
95111
:return bool:
@@ -122,14 +138,15 @@ def write_findings(self, scan_id: int, repository_id: int, scan_findings: List[F
122138
exit_code = self.exit_code_success
123139

124140
rule_tags = self._get_rule_tags()
141+
ignore_dictionary = self.ignore_findings_providers.get_ignore_list()
125142
for finding in scan_findings:
126143
should_process_finding = self._finding_tag_filter(finding, rule_tags, self.filter_tag)
127144
if should_process_finding:
128-
finding_action = self._determine_finding_action(finding, rule_tags)
145+
finding_action = self._determine_finding_action(finding, rule_tags, ignore_dictionary)
129146
if finding_action == FindingAction.BLOCK:
130147
finding_action_value = colored(finding_action.value, "red", attrs=["bold"])
131148
block_count += 1
132-
elif finding_action == FindingAction.WARN:
149+
elif finding_action in [FindingAction.WARN, FindingAction.IGNORED]:
133150
finding_action_value = colored(finding_action.value, "light_red", attrs=["bold"])
134151
warn_count += 1
135152
elif finding_action == FindingAction.INFO:
@@ -140,7 +157,8 @@ def write_findings(self, scan_id: int, repository_id: int, scan_findings: List[F
140157
info_count += 1
141158

142159
if exit_code != self.exit_code_block:
143-
if exit_code == self.exit_code_success and finding_action == FindingAction.WARN:
160+
if exit_code == self.exit_code_success and \
161+
finding_action in [FindingAction.WARN, FindingAction.IGNORED]:
144162
exit_code = self.exit_code_warn
145163
elif finding_action == FindingAction.BLOCK:
146164
exit_code = self.exit_code_block
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
file_path_1|rule_1|1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#commented_path|commented_rule|commented_line_number|expiry_date
2+
empty_line
3+
expired_path|expired_rule|38|2022-09-09T15:30:30
4+
wrong_date_path|wrong_date_rule|38|2022X09-09T15:30:30
5+
active_path|active_rule|57|2122-09-09T15:30:30
6+
active_path_2|active_rule_2|58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
title = "gitleaks config"
2+
3+
version = "2.0.13"
4+
5+
[[rules]]
6+
id = "rule_1"
7+
description = "Fake rule"
8+
regex = '''something'''
9+
tags = ["Block"]
10+
11+
[[rules]]
12+
id = "rule_2"
13+
description = "Another Fake rule"
14+
regex = '''something'''
15+
tags = ["Block"]
16+
17+
[[rules]]
18+
id = "rule_3"
19+
description = "Another Fake rule again"
20+
regex = '''something'''
21+
tags = ["Warn"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Standard Library
2+
from pathlib import Path
3+
4+
# First Party
5+
from vcs_scanner.secret_scanners.ignore_list_provider import IgnoredListProvider
6+
7+
THIS_DIR = Path(__file__).parent.parent
8+
9+
10+
# We check that given a file, we get only 1 line:
11+
def test_ignore_list_provider():
12+
ignore_list_path = THIS_DIR.parent / "fixtures/ignore-findings-list.dsv"
13+
listProvider = IgnoredListProvider(str(ignore_list_path))
14+
assert {'active_path|active_rule|57': True,
15+
'active_path_2|active_rule_2|58': True} == listProvider.get_ignore_list()

0 commit comments

Comments
 (0)