Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reporting feature #387

Merged
merged 20 commits into from
Apr 13, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions detect_secrets/audit/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from . import analytics # noqa: F401
from . import report # noqa: F401
from .audit import audit_baseline # noqa: F401
from .compare import compare_baselines # noqa: F401
137 changes: 137 additions & 0 deletions detect_secrets/audit/report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import hashlib
import codecs
import json
from enum import Enum

from .io import print_message
from ..core.plugins.util import Plugin, get_mapping_from_secret_type_to_class
from ..core.scan import _get_lines_from_file, _scan_line
from ..core.potential_secret import PotentialSecret
from ..plugins.base import BasePlugin


class SecretClass(Enum):
TRUE_POSITIVE = 1
FALSE_POSITIVE = 2
UNKNOWN = 3

def from_boolean(is_secret: bool) -> Enum:
if is_secret == None:
return SecretClass.UNKNOWN
elif is_secret:
return SecretClass.TRUE_POSITIVE
else:
return SecretClass.FALSE_POSITIVE

def to_string(self) -> str:
return self.name

def get_prioritary(self, secret_class: str) -> Enum:
try:
to_compare = SecretClass[secret_class]
except:
return self
if to_compare.value < self.value:
return secret_class
else:
return self
pablosnt marked this conversation as resolved.
Show resolved Hide resolved


class SecretClassToPrint(Enum):
REAL_SECRET = 1
FALSE_POSITIVE = 2

def from_class(secret_class: SecretClass) -> Enum:
if secret_class in [SecretClass.UNKNOWN, SecretClass.TRUE_POSITIVE]:
return SecretClassToPrint.REAL_SECRET
else:
return SecretClassToPrint.FALSE_POSITIVE


def print_report(
baseline_file: str,
class_to_print: SecretClassToPrint = None
) -> None:
baseline = json.load(codecs.open(baseline_file, encoding='utf-8'))
details = get_secrets_details_from_baseline(baseline)
pablosnt marked this conversation as resolved.
Show resolved Hide resolved
plugins = get_mapping_from_secret_type_to_class()
secrets = {}
for filename, secret_type, secret_hash, is_secret in details:
secret_class = SecretClass.from_boolean(is_secret)
if class_to_print != None and SecretClassToPrint.from_class(secret_class) != class_to_print:
continue
try:
detections = get_potential_secrets(filename, plugins[secret_type](), secret_hash)
except:
continue
identifier = hashlib.sha512((secret_hash + filename).encode('utf-8')).hexdigest()
for detection in detections:
if identifier in secrets:
secrets[identifier]['lines'][detection.line_number] = get_line_content(filename, detection.line_number)
if not secret_type in secrets[identifier]['types']:
secrets[identifier]['types'].append(secret_type)
secrets[identifier]['class'] = secret_class.get_prioritary(secrets[identifier]['class']).to_string()
else:
finding = {}
finding['secret'] = detection.secret_value
finding['filename'] = filename
finding['lines'] = {}
finding['lines'][detection.line_number] = get_line_content(filename, detection.line_number)
finding['types'] = [secret_type]
finding['class'] = secret_class.to_string()
pablosnt marked this conversation as resolved.
Show resolved Hide resolved
secrets[identifier] = finding
pablosnt marked this conversation as resolved.
Show resolved Hide resolved

output = []
for identifier in secrets:
output.append(secrets[identifier])

print_message(json.dumps(output, indent=4, sort_keys=True))
pablosnt marked this conversation as resolved.
Show resolved Hide resolved


def get_secrets_details_from_baseline(
baseline: str
) -> [(str, str, str, bool)]:
"""
:returns: Details of each secret present in the baseline file.
"""
for filename, secrets in baseline['results'].items():
for secret in secrets:
yield filename, secret['type'], secret['hashed_secret'], secret['is_secret']


def get_secret_class(
is_secret: bool
) -> str:
"""
:returns: Secret class as string.
"""
return 'Unknown' if is_secret == None else 'True positive' if is_secret else 'False positive'


def get_potential_secrets(
filename: str,
plugin: Plugin,
secret_to_find: str
) -> [PotentialSecret]:
"""
:returns: List of PotentialSecrets detected by a specific plugin in a file.
"""
for lines in _get_lines_from_file(filename):
for line_number, line in list(enumerate(lines, 1)):
secrets = _scan_line(plugin, filename, line, line_number)
for secret in secrets:
if secret.secret_hash == secret_to_find:
yield secret


def get_line_content(
filename: str,
line_number: int
) -> str:
"""
:returns: Line content from filename by line number.
"""
content = codecs.open(filename, encoding='utf-8').read()
if not content:
return None
return content.splitlines()[line_number - 1]
pablosnt marked this conversation as resolved.
Show resolved Hide resolved
29 changes: 29 additions & 0 deletions detect_secrets/core/usage/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def add_audit_action(parent: argparse._SubParsersAction) -> argparse.ArgumentPar
)

_add_mode_parser(parser)
_add_report_parser(parser)
_add_statistics_module(parser)
return parser

Expand All @@ -46,6 +47,34 @@ def _add_mode_parser(parser: argparse.ArgumentParser) -> None:
)


def _add_report_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
'--report',
action='store_true',
help=(
'Displays a report with the secrets detected'
)
)

parser.add_argument(
'--only-real',
action='store_true',
help=(
'Only includes real secrets in the report'
)
)

parser.add_argument(
'--only-false',
action='store_true',
help=(
'Only includes false positives in the report'
)
)
pablosnt marked this conversation as resolved.
Show resolved Hide resolved




def _add_statistics_module(parent: argparse.ArgumentParser) -> None:
parser = parent.add_argument_group(
title='analytics',
Expand Down
7 changes: 7 additions & 0 deletions detect_secrets/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ def handle_audit_action(args: argparse.Namespace) -> None:
print(json.dumps(stats.json(), indent=2))
else:
print(str(stats))
elif args.report:
class_to_print = None
if args.only_real:
class_to_print = audit.report.SecretClassToPrint.REAL_SECRET
elif args.only_false:
class_to_print = audit.report.SecretClassToPrint.FALSE_POSITIVE
audit.report.print_report(args.filename[0], class_to_print)
else:
# Starts interactive session.
if args.diff:
Expand Down