Skip to content

Commit

Permalink
feat(output): add report output format (#401)
Browse files Browse the repository at this point in the history
* feat(output): add report output

* add report to output format CLI opt
  • Loading branch information
dclayton-godaddy authored Nov 22, 2022
1 parent c750412 commit 7b9d55a
Show file tree
Hide file tree
Showing 7 changed files with 373 additions and 6 deletions.
8 changes: 8 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ ENV PIP_DEFAULT_TIMEOUT=100 \
PIP_NO_CACHE_DIR=1 \
POETRY_VERSION=1.1.12

RUN apt-get update && apt-get upgrade -y && apt-get install make cmake gcc libssl-dev wget -y
RUN wget https://github.com/libgit2/libgit2/archive/refs/tags/v1.4.0.tar.gz -O libgit2-1.4.0.tar.gz \
&& tar xzf libgit2-1.4.0.tar.gz \
&& cd libgit2-1.4.0/ \
&& cmake . \
&& make \
&& make install
RUN apt-get install libffi-dev -y
RUN pip --no-cache-dir install "poetry==$POETRY_VERSION"
RUN python -m venv /venv

Expand Down
1 change: 1 addition & 0 deletions tartufo/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def get_command(self, ctx: click.Context, cmd_name: str) -> Optional[click.Comma
types.OutputFormat.Json.value,
types.OutputFormat.Compact.value,
types.OutputFormat.Text.value,
types.OutputFormat.Report.value,
]
),
default="text",
Expand Down
1 change: 1 addition & 0 deletions tartufo/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class OutputFormat(enum.Enum):
Text = "text" # pylint: disable=invalid-name
Json = "json" # pylint: disable=invalid-name
Compact = "compact" # pylint: disable=invalid-name
Report = "report" # pylint: disable=invalid-name


@dataclass
Expand Down
71 changes: 71 additions & 0 deletions tartufo/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import stat
import sys
import tempfile
from types import ModuleType
import uuid
from datetime import datetime
from functools import lru_cache, partial
Expand Down Expand Up @@ -50,6 +51,74 @@ def del_rw(_func: Callable, name: str, _exc: Exception) -> None:
os.remove(name)


def get_version() -> str:
metadata: Optional[ModuleType]
try:
from importlib import metadata # type: ignore # pylint: disable=import-outside-toplevel
except ImportError:
# Python < 3.8
import importlib_metadata as metadata # type: ignore # pylint: disable=import-outside-toplevel

if metadata:
return metadata.version(__package__) # type: ignore
return ""


def echo_report_result(scanner: "ScannerBase", now: str):
click.echo(f"Tartufo Scan Results (Time: {now})")
for issue in scanner.scan():
click.echo(str(issue))
if scanner.issue_count == 0:
click.echo("All clear. No secrets detected.")

click.echo("\nConfiguration:")
version = get_version()
click.echo(f" version: {version}")
if scanner.global_options.entropy:
click.echo(" entropy: Enabled")
click.echo(f" sensitivity: {scanner.global_options.entropy_sensitivity}")
else:
click.echo(" entropy: Disabled")
click.echo(
f" regex: {'Enabled' if scanner.global_options.regex else 'Disabled'}"
)

click.echo("\nExcluded paths:")

if scanner.global_options.exclude_path_patterns:
for item in scanner.global_options.exclude_path_patterns:
if isinstance(item, dict):
path_pattern = item.get("path-pattern")
reason = item.get("reason")
else:
path_pattern = item
reason = "Unknown reason"
click.echo(f" {path_pattern}: {reason}")

click.echo("\nExcluded signatures:")
if scanner.global_options.exclude_signatures:
for item in scanner.global_options.exclude_signatures:
if isinstance(item, dict):
signature = item.get("signature")
reason = item.get("reason")
else:
signature = item
reason = "Unknown reason"

click.echo(f" {signature}: {reason}")

click.echo("\nExcluded entropy patterns:")
for e_item in scanner.excluded_entropy:
pattern = e_item.pattern.pattern if e_item.pattern else ""
path_pattern = e_item.path_pattern.pattern if e_item.path_pattern else ""
m_scope = e_item.re_match_scope.value if e_item.re_match_scope else ""
m_type = e_item.re_match_type.value if e_item.re_match_type else ""
reason = e_item.name
click.echo(
f" {pattern} (path={path_pattern}, scope={m_scope}, type={m_type}): {reason}"
)


def echo_result(
options: "types.GlobalOptions",
scanner: "ScannerBase",
Expand Down Expand Up @@ -102,6 +171,8 @@ def echo_result(
f"[{issue.issue_type.value}] {issue.chunk.file_path}: {issue.matched_string} "
f"({issue.signature}, {issue.issue_detail})"
)
elif options.output_format == types.OutputFormat.Report.value:
echo_report_result(scanner, now)
else:
for issue in scanner.scan():
click.echo(str(issue))
Expand Down
7 changes: 7 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pathlib import Path
import platform
from dataclasses import fields
from typing import Type, TypeVar
Expand All @@ -7,11 +8,17 @@
PY_36 = ("3", "6", "0") <= PY_VERSION < ("3", "7", "0")
PY_37 = ("3", "7", "0") <= PY_VERSION < ("3", "8", "0")
BROKEN_USER_PATHS = WINDOWS and (PY_36 or PY_37)
REPO_ROOT_PATH = Path(__file__).parent.parent
DATA_PATH = REPO_ROOT_PATH.joinpath("tests", "data")


OptionsType = TypeVar("OptionsType") # pylint: disable=invalid-name


def get_data_path(*added_paths: str) -> Path:
return Path.joinpath(DATA_PATH, *added_paths)


def generate_options(option_class: Type[OptionsType], **kwargs) -> OptionsType:
option_args = {field.name: None for field in fields(option_class)}
option_args.update(kwargs)
Expand Down
13 changes: 7 additions & 6 deletions tests/test_scan_local_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,28 @@ def test_scan_exits_gracefully_when_target_is_not_git_repo(self):
)

def test_new_file_shows_up(self):
file_name = "secret_1.key"
file_name = helpers.get_data_path("config", "secret_1.key")
runner = CliRunner()
# Add file with high entropy
secret_key = sha256(b"hello world")
with open(file_name, "a") as file:
with open(file_name.absolute(), "a") as file:
file.write(secret_key.hexdigest())
repo = Repository(".")
repo = Repository(helpers.REPO_ROOT_PATH)

# Check that tartufo picks up on newly added files
repo.index.add("tests/data/config/" + file_name)
file_name_relative = "tests/data/config/secret_1.key"
repo.index.add(file_name_relative)
repo.index.write() # This actually writes the index to disk. Without it, the tracked file is not actually staged.
result = runner.invoke(cli.main, ["--entropy-sensitivity", "1", "pre-commit"])
self.assertNotEqual(result.exit_code, 0)

# Cleanup
repo.index.remove("tests/data/config/" + file_name)
repo.index.remove(file_name_relative)
repo.index.write() # This actually writes the index to disk. Without it, secret.key will not removed from the index.
remove(file_name)

def test_new_unstaged_file_does_not_show_up(self):
file_name = "secret_2.key"
file_name = helpers.get_data_path("secret_2.key")
runner = CliRunner()
# Add file with high entropy
secret_key = sha256(b"hello world")
Expand Down
Loading

0 comments on commit 7b9d55a

Please sign in to comment.