Skip to content

Commit

Permalink
DB2 Detector (Yelp#194)
Browse files Browse the repository at this point in the history
Supports git-defenders/detect-secrets-discuss#190

DB2 Verification (Yelp#196)

Supports git-defenders/detect-secrets-discuss#190

Use DB2 detector (Yelp#199)

Supports git-defenders/detect-secrets-discuss#190

Refactor DB2 verification for calling externally (Yelp#203)

Supports fixing bug [here](https://github.ibm.com/git-defenders/detect-secrets-stream/blob/master/detect_secrets_stream/validation/db2.py#L25)

Catch DB2 hostname, port, database from connection url (Yelp#209)

Supports git-defenders/detect-secrets-discuss#212

Timeout DB2 detector if it takes too long (Yelp#214)
  • Loading branch information
justineyster committed Sep 9, 2020
1 parent 14ba284 commit b88ad9e
Show file tree
Hide file tree
Showing 15 changed files with 550 additions and 14 deletions.
7 changes: 3 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,9 @@ matrix:
python: 3.6
- env: TOXENV=py37
python: 3.7
dist: xenial # Required for Python >= 3.7 (travis-ci/travis-ci#9069)
- env: TOXENV=py38
python: 3.8
dist: xenial # Required for Python >= 3.7 (travis-ci/travis-ci#9069)
dist: xenial # required for Python >= 3.7 (travis-ci/travis-ci#9069)
# - env: TOXENV=pypy
# python: pypy
install:
- pip install tox
script: make test && docker build -t $DOCKER_IMAGE:$DOCKER_IMAGE_TAG --no-cache .
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ RUN apk add --no-cache jq git curl bash openssl
RUN mkdir -p /code
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN apk add --no-cache --virtual .build-deps gcc musl-dev
RUN pip install cython
RUN easy_install /usr/src/app
WORKDIR /code
ENTRYPOINT [ "/usr/src/app/run-scan.sh" ]
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
PROJECT_DIR := $(shell pwd)

.PHONY: minimal
minimal: setup

Expand All @@ -22,3 +24,13 @@ clean:
super-clean: clean
rm -rf .tox
rm -rf venv

.PHONY: fix-db2-mac
fix-db2-mac:
# comment out lines for any interpreters that aren't installed on your machine
install_name_tool -change libdb2.dylib $(PROJECT_DIR)/.tox/py27/lib/python2.7/site-packages/clidriver/lib/libdb2.dylib $(PROJECT_DIR)/.tox/py27/lib/python2.7/site-packages/ibm_db.so
install_name_tool -change libbd2.dylib $(PROJECT_DIR)/.tox/py35/lib/python3.5/site-packages/clidriver/lib/libdb2.dylib $(PROJECT_DIR)/.tox/py35/lib/python3.5/site-packages/ibm_db.cpython-35m-darwin.so
install_name_tool -change libbd2.dylib $(PROJECT_DIR)/.tox/py36/lib/python3.6/site-packages/clidriver/lib/libdb2.dylib $(PROJECT_DIR)/.tox/py36/lib/python3.6/site-packages/ibm_db.cpython-36m-darwin.so
install_name_tool -change libdb2.dylib $(PROJECT_DIR)/.tox/py37/lib/python3.7/site-packages/clidriver/lib/libdb2.dylib $(PROJECT_DIR)/.tox/py37/lib/python3.7/site-packages/ibm_db.cpython-37m-darwin.so
install_name_tool -change libdb2.dylib $(PROJECT_DIR)/.tox/pypy/site-packages/clidriver/lib/libdb2.dylib $(PROJECT_DIR)/.tox/pypy/site-packages/ibm_db.pypy-41.so
install_name_tool -change libdb2.dylib $(PROJECT_DIR)/.tox/pypy3/site-packages/clidriver/lib/libdb2.dylib $(PROJECT_DIR)/.tox/pypy3/site-packages/ibm_db.pypy3-71-darwin.so
6 changes: 6 additions & 0 deletions detect_secrets/core/usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,12 @@ class PluginOptions:
disable_help_text='Disable scanning for SoftLayer keys',
is_default=True,
),
PluginDescriptor(
classname='DB2Detector',
disable_flag_text='--no-db2-scan',
disable_help_text='Disable scanning for DB2 credentials',
is_default=True,
),
]

default_plugins_list = [
Expand Down
1 change: 1 addition & 0 deletions detect_secrets/plugins/common/initialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from ..base import BasePlugin
from ..basic_auth import BasicAuthDetector # noqa: F401
from ..common.util import get_mapping_from_secret_type_to_class_name
from ..db2 import DB2Detector # noqa: F401
from ..gh import GHDetector # noqa: F401
from ..high_entropy_strings import Base64HighEntropyString # noqa: F401
from ..high_entropy_strings import HexHighEntropyString # noqa: F401
Expand Down
26 changes: 18 additions & 8 deletions detect_secrets/plugins/common/util.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import importlib.util
import inspect
import os
from abc import abstractproperty
from functools import lru_cache

from detect_secrets.plugins.base import BasePlugin
from detect_secrets.util import get_root_directory
try:
from functools import lru_cache
except ImportError: # pragma: no cover
from functools32 import lru_cache

# These plugins need to be imported here so that globals()
# can find them.
from ..artifactory import ArtifactoryDetector # noqa: F401
from ..aws import AWSKeyDetector # noqa: F401
from ..base import BasePlugin
from ..basic_auth import BasicAuthDetector # noqa: F401
from ..db2 import DB2Detector # noqa: F401
from ..high_entropy_strings import Base64HighEntropyString # noqa: F401
from ..high_entropy_strings import HexHighEntropyString # noqa: F401
from ..keyword import KeywordDetector # noqa: F401
from ..private_key import PrivateKeyDetector # noqa: F401
from ..slack import SlackDetector # noqa: F401
from ..stripe import StripeDetector # noqa: F401


@lru_cache(maxsize=1)
Expand Down
185 changes: 185 additions & 0 deletions detect_secrets/plugins/db2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
from __future__ import absolute_import

import re

import ibm_db

from .base import RegexBasedDetector
from detect_secrets.core.constants import VerifiedResult


class DB2Detector(RegexBasedDetector):

secret_type = 'DB2 Credentials'

begin = r'(?:(?<=\W)|(?<=^))'
opt_quote = r'(?:"|\'|)'
opt_db = r'(?:db2|dashdb|db|)'
opt_dash_undrscr = r'(?:_|-|)'
password_keyword = r'(?:password|pwd|pass|passwd)'
opt_space = r'(?: *)'
assignment = r'(?:=|:|:=|=>|::)'
# catch any character except newline and quotations, we exclude these
# because the regex will erronously match them when present at the end of the password
# db2 password requirements vary by version so we cast a broad net
password = r'([^\n"\']+)'
denylist = (
re.compile(
r'{begin}{opt_quote}{opt_db}{opt_dash_undrscr}{password_keyword}{opt_quote}{opt_space}'
'{assignment}{opt_space}{opt_quote}{password}{opt_quote}'.format(
begin=begin,
opt_quote=opt_quote,
opt_db=opt_db,
opt_dash_undrscr=opt_dash_undrscr,
password_keyword=password_keyword,
opt_space=opt_space,
assignment=assignment,
password=password,
), flags=re.IGNORECASE,
),
)

username_keyword_regex = r'(?:user|user(?:_|-|)name|uid|user(?:_|-|)id|u(?:_|-|)name)'
username_regex = r'([a-zA-Z0-9_]+)'

database_keyword_regex = r'(?:database|db|database(?:_|-|)name|db(?:_|-|)name)'
database_regex = r'([a-zA-Z0-9_-]+)'

port_keyword_regex = r'(?:port|port(?:_|-|)number)'
port_regex = r'([0-9]{1,5})'

hostname_keyword_regex = (
r'(?:host|host(?:_|-|)name|host(?:_|-|)address|'
r'host(?:_|-|)ip|host(?:_|-|)ip(?:_|-|)address)'
)
hostname_regex = (
r'((?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)'
r'*(?:.\[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))'
)

def verify(self, token, content, potential_secret, timeout=5):

username_matches = find_other_factor(
content, self.username_keyword_regex,
self.username_regex,
)
if not username_matches:
return VerifiedResult.UNVERIFIED

database_matches = find_other_factor(
content, self.database_keyword_regex,
self.database_regex,
)
port_matches = find_other_factor(
content, self.port_keyword_regex,
self.port_regex,
)
hostname_matches = find_other_factor(
content, self.hostname_keyword_regex,
self.hostname_regex,
)

url_matches = get_hostname_port_database_from_url(
content, self.hostname_regex, self.port_regex, self.database_regex,
)
for match in url_matches:
hostname, port, database = match
hostname_matches.append(hostname)
port_matches.append(port)
database_matches.append(database)

if not database_matches or not port_matches or not hostname_matches:
return VerifiedResult.UNVERIFIED

for username in username_matches: # pragma: no cover
for database in database_matches: # pragma: no cover
for port in port_matches: # pragma: no cover
for hostname in hostname_matches: # pragma: no cover
verify_result = verify_db2_credentials(
database, hostname, port, username, token, timeout,
)
if verify_result == VerifiedResult.VERIFIED_TRUE:
potential_secret.other_factors['database'] = database
potential_secret.other_factors['hostname'] = hostname
potential_secret.other_factors['port'] = port
potential_secret.other_factors['username'] = username
return verify_result

return VerifiedResult.VERIFIED_FALSE


def verify_db2_credentials(
database, hostname, port, username, password, timeout=5,
): # pragma: no cover
try:
conn_str = 'database={database};hostname={hostname};port={port};' + \
'protocol=tcpip;uid={username};pwd={password};' + \
'ConnectTimeout={timeout}'
conn_str = conn_str.format(
database=database,
hostname=hostname,
port=port,
username=username,
password=password,
timeout=timeout,
)
ibm_db_conn = ibm_db.connect(conn_str, '', '')
if ibm_db_conn:
return VerifiedResult.VERIFIED_TRUE
else:
return VerifiedResult.VERIFIED_FALSE
except Exception as e:
if 'Timeout' in str(e):
return VerifiedResult.UNVERIFIED
else:
return VerifiedResult.VERIFIED_FALSE


def find_other_factor(content, factor_keyword_regex, factor_regex):
begin = r'(?:(?<=\W)|(?<=^))'
opt_quote = r'(?:"|\'|)'
opt_db = r'(?:db2|dashdb|db|)'
opt_dash_undrscr = r'(?:_|-|)'
opt_space = r'(?: *)'
assignment = r'(?:=|:|:=|=>|::)'
regex = re.compile(
r'{begin}{opt_quote}{opt_db}{opt_dash_undrscr}{factor_keyword}{opt_quote}{opt_space}'
'{assignment}{opt_space}{opt_quote}{factor}{opt_quote}'.format(
begin=begin,
opt_quote=opt_quote,
opt_db=opt_db,
opt_dash_undrscr=opt_dash_undrscr,
factor_keyword=factor_keyword_regex,
opt_space=opt_space,
assignment=assignment,
factor=factor_regex,
), flags=re.IGNORECASE,
)

return [
match
for line in content.splitlines()
for match in regex.findall(line)
]


def get_hostname_port_database_from_url(content, hostname_regex, port_regex, database_regex):
"""
Gets hostname, port, and database factors from a jdbc db2 url
Accepts: content to scan, regexes to capture hostname, port, and database
Returns: list of tuples of format (hostname, port, database),
or empty list if no matches
"""
regex = re.compile(
r'jdbc:db2:\/\/{hostname}:{port}\/{database}'.format(
hostname=hostname_regex,
port=port_regex,
database=database_regex,
),
)

return [
(match[0], match[1], match[2])
for line in content.splitlines()
for match in regex.findall(line)
]
2 changes: 1 addition & 1 deletion detect_secrets/plugins/gh.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class GHDetector(RegexBasedDetector):
secret_type = 'GitHub Credentials'

opt_github = r'(?:github|gh|ghe|git|)'
opt_space = r'(?: |)'
opt_space = r'(?: *)'
opt_quote = r'(?:"|\'|)'
opt_assignment = r'(?:=|:|:=|=>|)'
opt_dash_undrscr = r'(?:_|-|)'
Expand Down
2 changes: 1 addition & 1 deletion detect_secrets/plugins/softlayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class SoftLayerDetector(RegexBasedDetector):
opt_dash_undrscr = r'(?:_|-|)'
opt_api = r'(?:api|)'
key_or_pass = r'(?:key|pwd|password|pass|token)'
opt_space = r'(?: |)'
opt_space = r'(?: *)'
opt_assignment = r'(?:=|:|:=|=>|)'
secret = r'([a-z0-9]{64})'
denylist = [
Expand Down
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ tox-pip-extensions
tox>=3.8
unidiff
responses
ibm_db
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
install_requires=[
'pyyaml',
'requests',
'ibm_db',
],
extras_require={
'word_list': [
Expand Down
1 change: 1 addition & 0 deletions tests/core/usage_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def test_consolidates_output_basic(self):
'ArtifactoryDetector': {},
'GHDetector': {},
'SoftLayerDetector': {},
'DB2Detector': {},
}

def test_consolidates_removes_disabled_plugins(self):
Expand Down
16 changes: 16 additions & 0 deletions tests/main_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ def test_scan_string_basic_default(
AWSKeyDetector : False
ArtifactoryDetector: False
BasicAuthDetector : False
DB2Detector : False
GHDetector : False
PrivateKeyDetector : False
SlackDetector : False
Expand Down Expand Up @@ -355,6 +356,9 @@ def test_old_baseline_ignored_with_update_flag(
{
'name': 'BasicAuthDetector',
},
{
'name': 'DB2Detector',
},
{
'name': 'GHDetector',
},
Expand Down Expand Up @@ -398,6 +402,9 @@ def test_old_baseline_ignored_with_update_flag(
{
'name': 'BasicAuthDetector',
},
{
'name': 'DB2Detector',
},
{
'name': 'GHDetector',
},
Expand Down Expand Up @@ -498,6 +505,9 @@ def test_old_baseline_ignored_with_update_flag(
{
'name': 'BasicAuthDetector',
},
{
'name': 'DB2Detector',
},
{
'name': 'GHDetector',
},
Expand Down Expand Up @@ -540,6 +550,9 @@ def test_old_baseline_ignored_with_update_flag(
{
'name': 'BasicAuthDetector',
},
{
'name': 'DB2Detector',
},
{
'name': 'GHDetector',
},
Expand Down Expand Up @@ -681,6 +694,9 @@ def test_scan_with_default_plugin(self):
{
'name': 'BasicAuthDetector',
},
{
'name': 'DB2Detector',
},
{
'name': 'GHDetector',
},
Expand Down
Loading

0 comments on commit b88ad9e

Please sign in to comment.