Skip to content

Commit

Permalink
feat: STFT-076: Secureli ignore - ability to suppress issues - auto p…
Browse files Browse the repository at this point in the history
…rompt user (#18)

[STFT-076: STFT-076: Secureli ignore - ability to suppress issues - auto
prompt user](https://slalom.atlassian.net/browse/STFT-76)

This PR adds a new feature which prompts users after a scan that has
resulted in a linter failure if they would like to add an ignore rule
for any of the detected failures.

If the user agrees to add an ignore rule, then secureli iterates through
each failure and asks the user if they want to add an ignore for that
failure, if so, then it also asks if the user wants to ignore the
failure for all files or a specific file.

To test this, make any change (and stage the file) that would cause a
linter failure. Some simple ones would be adding some white space at the
end of a line or removing the blank line at the end of a file. Any
linter failure should trigger the notification.

Note: New ignore rules are detected as an available upgrade, so when you
run your next scan after adding an ignore, you will want to confirm the
upgrade.
  • Loading branch information
AldosAC authored Apr 6, 2023
1 parent b5fbbc1 commit c0178d3
Show file tree
Hide file tree
Showing 47 changed files with 1,143 additions and 135 deletions.
17 changes: 8 additions & 9 deletions .secureli.yaml
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

0 comments on commit c0178d3

Please sign in to comment.