Skip to content

Commit

Permalink
Merge pull request #61 from PaperMtn/feature/signature-disable
Browse files Browse the repository at this point in the history
Feature/signature disable
  • Loading branch information
PaperMtn authored Nov 18, 2024
2 parents 94929fb + adc9a26 commit d8455f3
Show file tree
Hide file tree
Showing 12 changed files with 257 additions and 159 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## [4.4.0] - 2024-11-12
### Added
- Ability to disable signatures by their ID in the `watchman.conf` config file.
- These signatures will not be used when running Slack Watchman
- Signature IDs for each signature can be found in the [Watchman Signatures repository](https://github.com/PaperMtn/watchman-signatures)

### Fixed
- Bug where variables were not being imported from watchman.conf config file

## [4.3.0] - 2024-10-27
### Changed
- Timestamps are now in UTC across all logging for consistency
Expand Down
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ slack-watchman --probe https://domain.slack.com
### Signatures
Slack Watchman uses custom YAML signatures to detect matches in Slack. 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.

#### Suppressing Signatures
You can define signatures that you want to disable when running Slack Watchman by adding their IDs to the `disabled_signatures` section of the `watchman.conf` file. For example:

```yaml
slack_watchman:
token: ...
cookie: ...
url: ...
disabled_signatures:
- tokens_generic_bearer_tokens
- tokens_generic_access_tokens
```
You can find the ID of a signature in the individual YAML files in [Watchman Signatures repository](https://github.com/PaperMtn/watchman-signatures).
### Logging
Slack Watchman gives the following logging options:
Expand Down Expand Up @@ -127,13 +142,16 @@ Slack Watchman will first try to get the Slack token (plus the cookie token and

If this fails it will try to load the token(s) from `.conf` file (see below).

#### .conf file
#### watchman.conf file
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:
```yaml
slack_watchman:
token: xoxp-xxxxxxxx
cookie: xoxd-%2xxxxx
url: https://xxxxx.slack.com
disabled_signatures:
- tokens_generic_bearer_tokens
- tokens_generic_access_tokens
```
Slack Watchman will look for this file at runtime, and use the configuration options from here. If you are not using cookie auth, leave `cookie` and `url` blank.

Expand Down
3 changes: 3 additions & 0 deletions docs/example.conf
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ slack_watchman:
token: xoxp-xxxxxxxx
cookies: xoxd-%2xxxxx
url: https://xxxxx.slack.com
disabled_signatures:
- tokens_generic_access_tokens
- tokens_generic_bearer_tokens
153 changes: 83 additions & 70 deletions src/slack_watchman/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import traceback
from importlib import metadata
from importlib.metadata import PackageMetadata
from typing import List

import yaml

Expand All @@ -14,77 +15,102 @@
exceptions,
watchman_processor
)
from slack_watchman.clients.slack_client import SlackClient
from slack_watchman.loggers import (
StdoutLogger,
JSONLogger,
export_csv,
init_logger
)
from slack_watchman.models import (
signature,
user,
workspace,
post,
conversation
conversation,
auth_vars
)
from slack_watchman.loggers import StdoutLogger, JSONLogger, export_csv
from slack_watchman.clients.slack_client import SlackClient

OUTPUT_LOGGER: JSONLogger


def validate_conf(path: str, cookie: bool) -> bool:
""" Check the file slack_watchman.conf exists
def validate_conf(path: str, cookie_auth: bool) -> auth_vars.AuthVars:
""" Validates configuration and authentication settings for Slack Watchman from either
a config file or environment variables.
Authentication tokens from Environment Variables take precedence over those
from the config file.
Additional configuration settings, such as suppressed signatures, are loaded from the config file.
Args:
path: Path for the .config file
cookie: Whether session:cookie auth is being used
cookie_auth: Whether session:cookie auth is being used
Returns:
True if the required config settings are present, False if not
AuthVars object containing the authentication details
Raises:
MissingEnvVarError: If a required environment variable is not set
MissingCookieEnvVarError: If required variables for cookie auth aren't set
MisconfiguredConfFileError: If the config file is not valid
"""

if not cookie:
if not os.environ.get('SLACK_WATCHMAN_TOKEN'):
if os.path.exists(f'{os.path.expanduser("~")}/slack_watchman.conf'):
OUTPUT_LOGGER.log('WARNING', 'Legacy slack_watchman.conf file detected. Renaming to watchman.conf')
os.rename(rf'{os.path.expanduser("~")}/slack_watchman.conf',
rf'{os.path.expanduser("~")}/watchman.conf')
try:
with open(path) as yaml_file:
return yaml.safe_load(yaml_file)['slack_watchman']
except:
raise exceptions.MisconfiguredConfFileError
elif os.path.exists(path):
try:
with open(path) as yaml_file:
return yaml.safe_load(yaml_file)['slack_watchman']
except:
raise exceptions.MisconfiguredConfFileError
# Check for legacy config file and rename if necessary
legacy_path = f'{os.path.expanduser("~")}/slack_watchman.conf'
if os.path.exists(legacy_path):
OUTPUT_LOGGER.log('WARNING', 'Legacy slack_watchman.conf file detected. Renaming to watchman.conf')
os.rename(legacy_path, path)

auth_info = auth_vars.AuthVars(
token=None,
cookie=None,
url=None,
disabled_signatures=None,
cookie_auth=cookie_auth
)

# Check if config file exists
if os.path.exists(path):
try:
with open(path) as yaml_file:
conf_details = yaml.safe_load(yaml_file)['slack_watchman']
auth_info.disabled_signatures = conf_details.get('disabled_signatures')
except Exception:
raise exceptions.MisconfiguredConfFileError

if not cookie_auth:
# First try SLACK_WATCHMAN_TOKEN env var
try:
auth_info.token = os.environ['SLACK_WATCHMAN_TOKEN']
except KeyError:
# Failing that, try to get SLACK_WATCHMAN_TOKEN from config
if conf_details.get('token'):
auth_info.token = conf_details.get('token')
else:
try:
os.environ['SLACK_WATCHMAN_TOKEN']
except:
raise exceptions.MissingEnvVarError('SLACK_WATCHMAN_TOKEN')
raise exceptions.MissingEnvVarError('SLACK_WATCHMAN_TOKEN')
else:
if os.path.exists(f'{os.path.expanduser("~")}/slack_watchman.conf'):
OUTPUT_LOGGER.log('WARNING', 'Legacy slack_watchman.conf file detected. Renaming to watchman.conf')
os.rename(rf'{os.path.expanduser("~")}/slack_watchman.conf',
rf'{os.path.expanduser("~")}/watchman.conf')
try:
with open(path) as yaml_file:
return yaml.safe_load(yaml_file)['slack_watchman']
except:
raise exceptions.MisconfiguredConfFileError
elif os.path.exists(path):
try:
with open(path) as yaml_file:
return yaml.safe_load(yaml_file)['slack_watchman']
except:
raise exceptions.MisconfiguredConfFileError
else:
try:
os.environ['SLACK_WATCHMAN_COOKIE']
except:
raise exceptions.MissingEnvVarError('SLACK_WATCHMAN_COOKIE')
# First try SLACK_WATCHMAN_COOKIE and SLACK_WATCHMAN_URL env vars
try:
auth_info.cookie = os.environ['SLACK_WATCHMAN_COOKIE']
auth_info.url = os.environ['SLACK_WATCHMAN_URL']
except KeyError as e:
# Failing that, try to get SLACK_WATCHMAN_COOKIE and SLACK_WATCHMAN_URL from config
if conf_details.get('cookie') and conf_details.get('url'):
auth_info.cookie = conf_details.get('cookie')
auth_info.url = conf_details.get('url')
else:
raise exceptions.MissingCookieEnvVarError(e.args[0])
return auth_info


def supress_disabled_signatures(signatures: List[signature.Signature],
disabled_signatures: List[str]) -> List[signature.Signature]:
""" Supress signatures that are disabled in the config file
Args:
signatures: List of signatures to filter
disabled_signatures: List of signatures to disable
Returns:
List of signatures with disabled signatures removed
"""

try:
os.environ['SLACK_WATCHMAN_URL']
except:
raise exceptions.MissingEnvVarError('SLACK_WATCHMAN_URL')
return [sig for sig in signatures if sig.id not in disabled_signatures]


def search(slack_connection: SlackClient,
Expand Down Expand Up @@ -177,22 +203,6 @@ def unauthenticated_probe(workspace_domain: str,
sys.exit(1)


def init_logger(logging_type: str, debug: bool) -> JSONLogger | StdoutLogger:
""" Create a logger object. Defaults to stdout if no option is given
Args:
logging_type: Type of logging to use
debug: Whether to use debug level logging or not
Returns:
Logger object
"""

if not logging_type or logging_type == 'stdout':
return StdoutLogger(debug=debug)
else:
return JSONLogger(debug=debug)


def main():
global OUTPUT_LOGGER
try:
Expand Down Expand Up @@ -270,8 +280,8 @@ def main():
unauthenticated_probe(probe_domain, project_metadata)

conf_path = f'{os.path.expanduser("~")}/watchman.conf'
validate_conf(conf_path, cookie)
slack_con = watchman_processor.initiate_slack_connection(cookie)
auth_info = validate_conf(conf_path, cookie)
slack_con = watchman_processor.initiate_slack_connection(auth_info)

auth_data = slack_con.get_auth_test()
calling_user = user.create_from_dict(
Expand All @@ -286,6 +296,9 @@ def main():
OUTPUT_LOGGER.log('INFO', f'Workspace URL: {workspace_information.url}')
OUTPUT_LOGGER.log('INFO', 'Downloading and importing signatures...')
signature_list = signature_downloader.SignatureDownloader(OUTPUT_LOGGER).download_signatures()
signature_list = supress_disabled_signatures(signature_list, auth_info.disabled_signatures)
if auth_info.disabled_signatures:
OUTPUT_LOGGER.log('INFO', f'The following signatures have been suppressed: {auth_info.disabled_signatures}')
OUTPUT_LOGGER.log('SUCCESS', f'{len(signature_list)} signatures loaded')
if cookie:
OUTPUT_LOGGER.log('SUCCESS', 'Successfully authenticated using cookie')
Expand Down
11 changes: 11 additions & 0 deletions src/slack_watchman/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ def __init__(self, env_var):
super().__init__(self.message)


class MissingCookieEnvVarError(Exception):
""" Exception raised when a cookie environment variable is missing.
"""

def __init__(self, env_var):
self.env_var = env_var
self.message = (f'Cookie authentication has been selected, but missing'
f'required environment variable: {self.env_var}')
super().__init__(self.message)


class MisconfiguredConfFileError(Exception):
""" Exception raised when the config file watchman.conf is missing.
"""
Expand Down
16 changes: 16 additions & 0 deletions src/slack_watchman/loggers.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,3 +345,19 @@ def export_csv(csv_name: str, export_data: List[IsDataclass]) -> None:
f.close()
except Exception as e:
print(e)


def init_logger(logging_type: str, debug: bool) -> JSONLogger | StdoutLogger:
""" Create a logger object. Defaults to stdout if no option is given
Args:
logging_type: Type of logging to use
debug: Whether to use debug level logging or not
Returns:
Logger object
"""

if not logging_type or logging_type == 'stdout':
return StdoutLogger(debug=debug)
else:
return JSONLogger(debug=debug)
12 changes: 12 additions & 0 deletions src/slack_watchman/models/auth_vars.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from dataclasses import dataclass
from typing import Optional, List


@dataclass
class AuthVars:
""" Class for managing authentication and configuration variables """
token: Optional[str] | None
cookie: Optional[str] | None
url: Optional[str] | None
disabled_signatures: Optional[List[str]] | None
cookie_auth: bool
4 changes: 4 additions & 0 deletions src/slack_watchman/models/signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Signature:
They also contain regex patterns to validate data that is found"""

name: str
id: str
status: str
author: str
date: str | datetime.date | datetime.datetime
Expand All @@ -39,6 +40,8 @@ class Signature:
def __post_init__(self):
if self.name and not isinstance(self.name, str):
raise TypeError(f'Expected `name` to be of type str, received {type(self.name).__name__}')
if self.id and not isinstance(self.id, str):
raise TypeError(f'Expected `id` to be of type str, received {type(self.id).__name__}')
if self.status and not isinstance(self.status, str):
raise TypeError(f'Expected `status` to be of type str, received {type(self.status).__name__}')
if self.author and not isinstance(self.author, str):
Expand Down Expand Up @@ -82,6 +85,7 @@ def create_from_dict(signature_dict: Dict[str, Any]) -> Signature:

return Signature(
name=signature_dict.get('name'),
id=(signature_dict.get('id')),
status=signature_dict.get('status'),
author=signature_dict.get('author'),
date=signature_dict.get('date'),
Expand Down
Loading

0 comments on commit d8455f3

Please sign in to comment.