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

feat: STFT-076: Secureli ignore - ability to suppress issues - auto prompt user #18

Merged
merged 16 commits into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 8 additions & 9 deletions .secureli.yaml
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of the settings in this file actually change, lines were re-ordered as a result of the file being written programmatically instead of by hand.

Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
echo:
level: ERROR
repo_files:
max_file_size: 1000000
exclude_file_patterns:
- .idea/
- .idea/
ignored_file_extensions:
- .pyc
- .drawio
- .png
- .jpg

echo:
level: ERROR
- .pyc
- .drawio
- .png
- .jpg
max_file_size: 1000000
Binary file removed dist/secureli-0.1.0-py3-none-any.whl
Binary file not shown.
Binary file removed dist/secureli-0.1.0.tar.gz
Binary file not shown.
2 changes: 1 addition & 1 deletion secureli/abstractions/pre_commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pydantic
import yaml

from secureli.settings import PreCommitSettings, PreCommitRepo
from secureli.repositories.settings import PreCommitSettings, PreCommitRepo
from secureli.utilities.patterns import combine_patterns
from secureli.resources.slugify import slugify

Expand Down
200 changes: 198 additions & 2 deletions secureli/actions/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,20 @@

from secureli.abstractions.echo import EchoAbstraction
from secureli.services.logging import LoggingService, LogAction
from secureli.services.scanner import ScanMode, ScannerService
from secureli.services.scanner import (
ScanMode,
ScannerService,
Failure,
OutputParseErrors,
)
from secureli.actions.action import VerifyOutcome, Action, ActionDependencies
from secureli.repositories.settings import (
SecureliRepository,
SecureliFile,
PreCommitSettings,
PreCommitRepo,
PreCommitHook,
)


class ScanAction(Action):
Expand All @@ -22,11 +34,13 @@ def __init__(
echo: EchoAbstraction,
logging: LoggingService,
scanner: ScannerService,
settings_repository: SecureliRepository,
):
super().__init__(action_deps)
self.scanner = scanner
self.echo = echo
self.logging = logging
self.settings = settings_repository

def scan_repo(
self,
Expand All @@ -51,11 +65,193 @@ def scan_repo(
return

scan_result = self.scanner.scan_repo(scan_mode, specific_test)

details = scan_result.output or "Unknown output during scan"
self.echo.print(details)

failure_count = len(scan_result.failures)
if failure_count > 0:
self._process_failures(scan_result.failures, always_yes=always_yes)

if not scan_result.successful:
self.echo.print(details)
self.logging.failure(LogAction.scan, details)
else:
self.echo.print("Scan executed successfully and detected no issues!")
self.logging.success(LogAction.scan)

def _process_failures(
self,
failures: list[Failure],
always_yes: bool,
):
"""
Processes any failures found during the scan.
:param failures: List of Failure objects representing linter failures
:param always_yes: Assume "Yes" to all prompts
"""
settings = self.settings.load()

ignore_fail_prompt = "Failures detected during scan.\n"
ignore_fail_prompt += "Add an ignore rule?"

# Ask if the user wants to ignore a failure
if always_yes:
always_yes_warning = "Hook failures were detected but the scan was initiated with the 'yes' flag.\n"
always_yes_warning += "SeCureLI cannot automatically add ignore rules with the 'yes' flag enabled.\n"
always_yes_warning += "Re-run your scan without the 'yes' flag to add an ignore rule for one of the\n"
always_yes_warning += "detected failures."

self.echo.print(always_yes_warning)
elif self.echo.confirm(ignore_fail_prompt, default_response=False):
# verify pre_commit exists in settings file.
if not settings.pre_commit:
settings.pre_commit = PreCommitSettings()

for failure in failures:
add_ignore_for_id = self.echo.confirm(
"\nWould you like to add an ignore for the {} failure on {}?".format(
failure.id, failure.file
)
)
if failure.repo == OutputParseErrors.REPO_NOT_FOUND:
self._handle_repo_not_found(failure)
elif always_yes or add_ignore_for_id:
settings = self._add_ignore_for_failure(
failure=failure, always_yes=always_yes, settings_file=settings
)

self.settings.save(settings=settings)

def _add_ignore_for_failure(
self,
failure: Failure,
always_yes: bool,
settings_file: SecureliFile,
):
"""
Processes an individual failure and adds an ignore rule for either the entire
hook or a particular file.
:param failure: Failure object representing a rule failure during a scan
:param always_yes: Assume "Yes" to all prompts
:param settings_file: SecureliFile representing the contents of the .secureli.yaml file
"""
ignore_repo_prompt = "You can add an ignore rule for just this file, or you can add an ignore rule for all files.\n"
ignore_repo_prompt += (
"Would you like to ignore this failure for all files?".format(failure.id)
)
ignore_file_prompt = (
"\nWould you like to ignore this failure for just the {} file?".format(
failure.file
)
)

self.echo.print("\nAdding an ignore rule for: {}\n".format(failure.id))

if always_yes or self.echo.confirm(
message=ignore_repo_prompt, default_response=False
):
# ignore for all files
self.echo.print("Adding an ignore for all files.")
modified_settings = self._ignore_all_files(
failure=failure, settings_file=settings_file
)
else:
if always_yes or self.echo.confirm(ignore_file_prompt, False):
self.echo.print("Adding an ignore for {}".format(failure.file))
modified_settings = self._ignore_one_file(
failure=failure, settings_file=settings_file
)
else:
self.echo.print(
"\nSkipping {} failure on {}".format(failure.id, failure.file)
)
modified_settings = settings_file

return modified_settings

def _handle_repo_not_found(self, failure: Failure):
"""
Handles a REPO_NOT_FOUND error
:param failure: A Failure object representing the scan failure with a missing repo url
"""
id = failure.id
self.echo.print(
"Unable to add an ignore for {}, SeCureLI was unable to identify the repo it belongs to.".format(
failure.id
)
)
self.echo.print("Skipping {}".format(id))

def _ignore_all_files(self, failure: Failure, settings_file: SecureliFile):
"""
Supresses a hook for all files in this repo
:param failure: Failure object representing the failed hook
:param settings_file: SecureliFile representing the contents of the .secureli.yaml file
:return: Returns the settings file after modifications
"""
pre_commit_settings = settings_file.pre_commit
repos = pre_commit_settings.repos
repo_settings_index = next(
(index for (index, repo) in enumerate(repos) if repo.url == failure.repo),
None,
)

if repo_settings_index is not None:
repo_settings = pre_commit_settings.repos[repo_settings_index]
if failure.id not in repo_settings.suppressed_hook_ids:
repo_settings.suppressed_hook_ids.append(failure.id)
else:
repo_settings = PreCommitRepo(
url=failure.repo, suppressed_hook_ids=[failure.id]
)
repos.append(repo_settings)

self.echo.print(
"Added {} to the suppressed_hooks_ids list for the {} repo".format(
failure.id, failure.repo
)
)

return settings_file

def _ignore_one_file(self, failure: Failure, settings_file: SecureliFile):
"""
Adds the failed file to the file exemptions list for the failed hook
:param failure: Failure object representing the failed hook
:param settings_file: SecureliFile representing the contents of the .secureli.yaml file
"""
pre_commit_settings = settings_file.pre_commit
repos = pre_commit_settings.repos
repo_settings_index = next(
(index for (index, repo) in enumerate(repos) if repo.url == failure.repo),
None,
)

if repo_settings_index is not None:
repo_settings = pre_commit_settings.repos[repo_settings_index]
else:
repo_settings = PreCommitRepo(url=failure.repo)
repos.append(repo_settings)

hooks = repo_settings.hooks
hook_settings_index = next(
(index for (index, hook) in enumerate(hooks) if hook.id == failure.id),
None,
)

if hook_settings_index is not None:
hook_settings = hooks[hook_settings_index]
if failure.file not in hook_settings.exclude_file_patterns:
hook_settings.exclude_file_patterns.append(failure.file)
else:
self.echo.print(
"An ignore rule is already present for the {} file".format(
failure.file
)
)
else:
hook_settings = PreCommitHook(id=failure.id)
hook_settings.exclude_file_patterns.append(failure.file)
repo_settings.hooks.append(hook_settings)

return settings_file
4 changes: 4 additions & 0 deletions secureli/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from secureli.actions.update import UpdateAction
from secureli.repositories.repo_files import RepoFilesRepository
from secureli.repositories.secureli_config import SecureliConfigRepository
from secureli.repositories.settings import SecureliRepository
from secureli.resources import read_resource
from secureli.services.git_ignore import GitIgnoreService
from secureli.services.language_analyzer import LanguageAnalyzerService
Expand Down Expand Up @@ -63,6 +64,8 @@ class Container(containers.DeclarativeContainer):
"""
secureli_config_repository = providers.Factory(SecureliConfigRepository)

settings_repository = providers.Factory(SecureliRepository)

# Abstractions

"""The echo service, used to stylistically render text to the terminal"""
Expand Down Expand Up @@ -161,6 +164,7 @@ class Container(containers.DeclarativeContainer):
echo=echo,
logging=logging_service,
scanner=scanner_service,
settings_repository=settings_repository,
)

"""Update Action, representing what happens when the update command is invoked"""
Expand Down
Loading