Skip to content

Commit e264982

Browse files
authored
Merge pull request #13 from PaperMtn/feature/signature-disable
Feature/signature disable
2 parents 57e7238 + abc43c0 commit e264982

File tree

9 files changed

+99
-23
lines changed

9 files changed

+99
-23
lines changed

CHANGELOG.md

+11-8
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
1-
## [3.1.0] - 2024-11-x
2-
### Changed
3-
- Package management and deployment moved to Poetry
4-
- Docker build process improved using multi-stage builds. The Dockerfile now doesn't contain any unnecessary files, and is much smaller.
5-
- Refactor to separate GitLab client and Watchman processing into modules
6-
- Refactor to implement python-gitlab library for GitLab API calls, instead of the custom client used previously.
7-
- This change allows for more efficient and easier to read code, is more reliable, and also allows for enhancements to be added more easily in the future.
8-
1+
## [3.1.0] - 2024-11-18
92
### Added
103
- Signatures now loaded into memory instead of being saved to disk. This allows for running on read-only filesystems.
4+
- Ability to disable signatures by their ID in the watchman.conf config file.
5+
- These signatures will not be used when running Slack Watchman
6+
- Signature IDs for each signature can be found in the Watchman Signatures repository
117
- Tests for Docker build
128
- Enhanced deduplication of findings
139
- The same match should not be returned multiple times within the same scope. E.g. if a token is found in a commit, it should not be returned multiple times in the same commit.
1410
- All dates are now converted and logged in UTC
1511
- Unit tests added for models and utils
1612

13+
### Changed
14+
- Package management and deployment moved to Poetry
15+
- Docker build process improved using multi-stage builds. The Dockerfile now doesn't contain any unnecessary files, and is much smaller.
16+
- Refactor to separate GitLab client and Watchman processing into modules
17+
- Refactor to implement [python-gitlab](https://python-gitlab.readthedocs.io/) library for GitLab API calls, instead of the custom client used previously.
18+
- This change gives more efficient and easier to read code, is more reliable, and also allows for enhancements to be added more easily in the future.
19+
1720
### Fixed
1821
- Error when searching wiki-blobs
1922
- There would often be failures when trying to find projects or groups associated with blobs. This is now fixed by adding logic to check if the blob is associated with a project or group, and get the correct information accordingly.

README.md

+22
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ GitLab Watchman can enumerate potentially useful information from a GitLab insta
5555
### Signatures
5656
GitLab Watchman uses custom YAML signatures to detect matches in GitLab. These signatures are pulled from the central [Watchman Signatures repository](https://github.com/PaperMtn/watchman-signatures). Slack Watchman automatically updates its signature base at runtime to ensure its using the latest signatures to detect secrets.
5757

58+
#### Suppressing Signatures
59+
You can define signatures that you want to disable when running GitLab Watchman by adding their IDs to the `disabled_signatures` section of the `watchman.conf` file. For example:
60+
61+
```yaml
62+
gitlab_watchman:
63+
disabled_signatures:
64+
- tokens_generic_bearer_tokens
65+
- tokens_generic_access_tokens
66+
```
67+
68+
You can find the ID of a signature in the individual YAML files in [Watchman Signatures repository](https://github.com/PaperMtn/watchman-signatures).
69+
5870
### Logging
5971
6072
GitLab Watchman gives the following logging options:
@@ -106,6 +118,16 @@ You also need to provide the URL of your GitLab instance.
106118
#### Providing token & URL
107119
GitLab Watchman will get the GitLab token and URL from the environment variables `GITLAB_WATCHMAN_TOKEN` and `GITLAB_WATCHMAN_URL`.
108120

121+
### watchman.conf file
122+
Configuration options can be passed in a file named `watchman.conf` which must be stored in your home directory. The file should follow the YAML format, and should look like below:
123+
```yaml
124+
gitlab_watchman:
125+
disabled_signatures:
126+
- tokens_generic_bearer_tokens
127+
- tokens_generic_access_tokens
128+
```
129+
GitLab Watchman will look for this file at runtime, and use the configuration options from here.
130+
109131
## Installation
110132
You can install the latest stable version via pip:
111133

src/gitlab_watchman/__init__.py

+34-5
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
import traceback
99
from dataclasses import dataclass
1010
from importlib import metadata
11-
from typing import List
11+
from typing import List, Dict, Any
12+
13+
import yaml
1214

1315
from gitlab_watchman import watchman_processor
1416
from gitlab_watchman.clients.gitlab_client import GitLabAPIClient
@@ -19,7 +21,8 @@
1921
GitLabWatchmanNotAuthorisedError,
2022
GitLabWatchmanAuthenticationError,
2123
ElasticsearchMissingError,
22-
MissingEnvVarError
24+
MissingEnvVarError,
25+
MisconfiguredConfFileError
2326
)
2427
from gitlab_watchman.loggers import (
2528
JSONLogger,
@@ -100,7 +103,7 @@ def perform_search(search_args: SearchArgs):
100103
search(search_args, sig, scope)
101104

102105

103-
def validate_variables() -> bool:
106+
def validate_variables() -> Dict[str, Any]:
104107
""" Validate whether GitLab Watchman environment variables have been set
105108
106109
Returns:
@@ -112,8 +115,30 @@ def validate_variables() -> bool:
112115
for var in required_vars:
113116
if var not in os.environ:
114117
raise MissingEnvVarError(var)
118+
path = f'{os.path.expanduser("~")}/watchman.conf'
119+
if os.path.exists(path):
120+
try:
121+
with open(path) as yaml_file:
122+
conf_details = yaml.safe_load(yaml_file)['gitlab_watchman']
123+
return {
124+
'disabled_signatures': conf_details.get('disabled_signatures', [])
125+
}
126+
except Exception as e:
127+
raise MisconfiguredConfFileError from e
128+
return {}
129+
130+
131+
def supress_disabled_signatures(signatures: List[signature.Signature],
132+
disabled_signatures: List[str]) -> List[signature.Signature]:
133+
""" Supress signatures that are disabled in the config file
134+
Args:
135+
signatures: List of signatures to filter
136+
disabled_signatures: List of signatures to disable
137+
Returns:
138+
List of signatures with disabled signatures removed
139+
"""
115140

116-
return True
141+
return [sig for sig in signatures if sig.id not in disabled_signatures]
117142

118143

119144
# pylint: disable=too-many-locals, missing-function-docstring, global-variable-undefined
@@ -183,7 +208,8 @@ def main():
183208

184209
OUTPUT_LOGGER = init_logger(logging_type, debug)
185210

186-
validate_variables()
211+
config = validate_variables()
212+
disabled_signatures = config.get('disabled_signatures', [])
187213
gitlab_client = watchman_processor.initiate_gitlab_connection(
188214
os.environ.get('GITLAB_WATCHMAN_TOKEN'),
189215
os.environ.get('GITLAB_WATCHMAN_URL'))
@@ -204,6 +230,9 @@ def main():
204230

205231
OUTPUT_LOGGER.log('INFO', 'Downloading and importing signatures')
206232
signature_list = SignatureDownloader(OUTPUT_LOGGER).download_signatures()
233+
if len(disabled_signatures) > 0:
234+
signature_list = supress_disabled_signatures(signature_list, disabled_signatures)
235+
OUTPUT_LOGGER.log('INFO', f'The following signatures have been suppressed: {disabled_signatures}')
207236
OUTPUT_LOGGER.log('SUCCESS', f'{len(signature_list)} signatures loaded')
208237
OUTPUT_LOGGER.log('INFO', f'{multiprocessing.cpu_count() - 1} cores being used')
209238

src/gitlab_watchman/clients/gitlab_client.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ def inner_function(*args, **kwargs):
4242
elif e.response_code == 500:
4343
pass
4444
else:
45-
raise GitLabWatchmanGetObjectError(e.error_message, func) from e
46-
except IndexError as e:
47-
raise GitLabWatchmanGetObjectError('Object not found', func) from e
45+
raise GitLabWatchmanGetObjectError(e.error_message, func, args) from e
46+
except IndexError:
47+
pass
4848
except Exception as e:
4949
raise e
5050

@@ -112,7 +112,7 @@ def get_user_by_username(self, username: str) -> Dict[str, Any] | None:
112112
GitLabWatchmanNotAuthorisedError: If the user is not authorized to access the resource
113113
GitlabWatchmanGetObjectError: If an error occurs while getting the object
114114
"""
115-
return self.gitlab_client.users.list(username=username)[0].asdict()
115+
return self.gitlab_client.users.list(username=username, active=False, blocked=True)[0].asdict()
116116

117117
@exception_handler
118118
def get_settings(self) -> Dict[str, Any]:
@@ -272,7 +272,7 @@ def get_group_members(self, group_id: str) -> List[Dict]:
272272
GitLabWatchmanNotAuthorisedError: If the user is not authorized to access the resource
273273
GitLabWatchmanGetObjectError: If an error occurs while getting the object
274274
"""
275-
members = self.gitlab_client.groups.get(group_id).members.list(as_list=True)
275+
members = self.gitlab_client.groups.get(group_id).members.list(as_list=True, get_all=True)
276276
return [member.asdict() for member in members]
277277

278278
@exception_handler

src/gitlab_watchman/exceptions.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ class GitLabWatchmanGetObjectError(GitLabWatchmanError):
3636
""" Exception raised when an error occurs while getting a GitLab API object.
3737
"""
3838

39-
def __init__(self, error_message: str, func):
40-
super().__init__(f'GitLab get object error: {error_message} - Function: {func.__name__}')
39+
def __init__(self, error_message: str, func, arg):
40+
super().__init__(f'GitLab get object error: {error_message} - Function: {func.__name__} - Arg: {arg}')
4141
self.error_message = error_message
4242

4343

@@ -49,3 +49,12 @@ class GitLabWatchmanNotAuthorisedError(GitLabWatchmanError):
4949
def __init__(self, error_message: str, func):
5050
super().__init__(f'Not authorised: {error_message} - {func.__name__}')
5151
self.error_message = error_message
52+
53+
54+
class MisconfiguredConfFileError(Exception):
55+
""" Exception raised when the config file watchman.conf is missing.
56+
"""
57+
58+
def __init__(self):
59+
self.message = f"The file watchman.conf doesn't contain config details for GitLab Watchman"
60+
super().__init__(self.message)

src/gitlab_watchman/loggers.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ def log(self,
8080
if notify_type == "result":
8181
if scope == 'blobs':
8282
message = 'SCOPE: Blob' \
83-
f' AUTHOR: {message.get("commit").get("author_name")} - ' \
84-
f'{message.get("commit").get("author_email")}' \
8583
f' COMMITTED: {message.get("commit").get("committed_date")} \n' \
84+
f' AUTHOR: {message.get("commit").get("author_name")} ' \
85+
f'EMAIL: {message.get("commit").get("author_email")}\n' \
8686
f' FILENAME: {message.get("blob").get("basename")} \n' \
8787
f' URL: {message.get("project").get("web_url")}/-/blob/{message.get("blob").get("ref")}/' \
8888
f'{message.get("blob").get("filename")} \n' \

src/gitlab_watchman/models/signature.py

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class Signature:
1818
They also contain regex patterns to validate data that is found"""
1919

2020
name: str
21+
id: str
2122
status: str
2223
author: str
2324
date: str | datetime.date | datetime.datetime
@@ -33,6 +34,8 @@ class Signature:
3334
def __post_init__(self):
3435
if self.name and not isinstance(self.name, str):
3536
raise TypeError(f'Expected `name` to be of type str, received {type(self.name).__name__}')
37+
if self.id and not isinstance(self.id, str):
38+
raise TypeError(f'Expected `id` to be of type str, received {type(self.id).__name__}')
3639
if self.status and not isinstance(self.status, str):
3740
raise TypeError(f'Expected `status` to be of type str, received {type(self.status).__name__}')
3841
if self.author and not isinstance(self.author, str):
@@ -65,6 +68,7 @@ def create_from_dict(signature_dict: Dict[str, Any]) -> Signature:
6568

6669
return Signature(
6770
name=signature_dict.get('name'),
71+
id=signature_dict.get('id'),
6872
status=signature_dict.get('status'),
6973
author=signature_dict.get('author'),
7074
date=signature_dict.get('date'),

tests/unit/models/fixtures.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ class GitLabMockData:
476476

477477
MOCK_SIGNATURE_DICT = {
478478
'name': 'Akamai API Access Tokens',
479+
'id': 'akamai_api_access_tokens',
479480
'status': 'enabled',
480481
'author': 'PaperMtn',
481482
'date': '2023-12-22',
@@ -566,6 +567,7 @@ def mock_user():
566567
def mock_wiki_blob():
567568
return wiki_blob.create_from_dict(GitLabMockData.MOCK_WIKI_BLOB_DICT)
568569

570+
569571
@pytest.fixture
570572
def mock_signature():
571-
return signature.create_from_dict(GitLabMockData.MOCK_SIGNATURE_DICT)
573+
return signature.create_from_dict(GitLabMockData.MOCK_SIGNATURE_DICT)

tests/unit/models/test_unit_signature.py

+7
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ def test_signature_initialisation(mock_signature):
1111

1212
# Test that the signature object has the correct attributes
1313
assert mock_signature.name == GitLabMockData.MOCK_SIGNATURE_DICT.get('name')
14+
assert mock_signature.id == GitLabMockData.MOCK_SIGNATURE_DICT.get('id')
1415
assert mock_signature.status == GitLabMockData.MOCK_SIGNATURE_DICT.get('status')
1516
assert mock_signature.author == GitLabMockData.MOCK_SIGNATURE_DICT.get('author')
1617
assert mock_signature.date == GitLabMockData.MOCK_SIGNATURE_DICT.get('date')
@@ -27,6 +28,12 @@ def test_field_type():
2728
with pytest.raises(TypeError):
2829
test_signature = signature.create_from_dict(signature_dict)
2930

31+
# Test that correct error is raised when id is not a string
32+
signature_dict = copy.deepcopy(GitLabMockData.MOCK_SIGNATURE_DICT)
33+
signature_dict['id'] = 123
34+
with pytest.raises(TypeError):
35+
test_signature = signature.create_from_dict(signature_dict)
36+
3037
# Test that correct error is raised when status is not a string
3138
signature_dict = copy.deepcopy(GitLabMockData.MOCK_SIGNATURE_DICT)
3239
signature_dict['status'] = 123

0 commit comments

Comments
 (0)