Skip to content

Commit

Permalink
feat: (144) Add Directory Support -d --directory (#279)
Browse files Browse the repository at this point in the history
#144

Example commands using the new directory option:

```
secureli scan --directory /absolute/path/to/a/git/directory
secureli scan -d ./relative/path/to/a/git/directory
secureli init -d ./relative/path/to/a/git/directory
secureli update --directory /absolute/path/to/a/git/directory
```

---------

Co-authored-by: Adina <adina.micula@slalom.com>
  • Loading branch information
keikoskat and Adina committed Aug 21, 2023
1 parent 896bf6f commit d23f94b
Show file tree
Hide file tree
Showing 22 changed files with 188 additions and 87 deletions.
34 changes: 23 additions & 11 deletions secureli/abstractions/pre_commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,27 +54,27 @@ def __init__(
):
self.command_timeout_seconds = command_timeout_seconds

def install(self):
def install(self, folder_path: Path):
"""
Creates the pre-commit hook file in the .git directory so that `secureli scan` is run on each commit
"""

# Write pre-commit with invocation of `secureli scan`
pre_commit_hook = ".git/hooks/pre-commit"
pre_commit_hook = folder_path / ".git/hooks/pre-commit"
with open(pre_commit_hook, "w") as f:
f.write("#!/bin/sh\n")
f.write("secureli scan\n")

# Make pre-commit executable
f = Path(pre_commit_hook)
f.chmod(f.stat().st_mode | stat.S_IEXEC)
pre_commit_hook.chmod(pre_commit_hook.stat().st_mode | stat.S_IEXEC)

def execute_hooks(
self, all_files: bool = False, hook_id: Optional[str] = None
self, folder_path: Path, all_files: bool = False, hook_id: Optional[str] = None
) -> ExecuteResult:
"""
Execute the configured hooks against the repository, either against your staged changes
or all the files in the repo
:param folder_path: Indicates the git folder against which you run secureli
:param all_files: True if we want to scan all files, default to false, which only
scans our staged changes we're about to commit
:param hook_id: A specific hook to run. If None, all hooks will be run
Expand All @@ -94,7 +94,9 @@ def execute_hooks(
if hook_id:
subprocess_args.append(hook_id)

completed_process = subprocess.run(subprocess_args, stdout=subprocess.PIPE)
completed_process = subprocess.run(
subprocess_args, stdout=subprocess.PIPE, cwd=folder_path
)
output = (
completed_process.stdout.decode("utf8") if completed_process.stdout else ""
)
Expand All @@ -105,13 +107,15 @@ def execute_hooks(

def autoupdate_hooks(
self,
folder_path: Path,
bleeding_edge: bool = False,
freeze: bool = False,
repos: Optional[list] = None,
) -> ExecuteResult:
"""
Updates the precommit hooks but executing precommit's autoupdate command. Additional info at
https://pre-commit.com/#pre-commit-autoupdate
:param folder_path: Indicates the git folder against which you run secureli
:param bleeding_edge: True if updating to the bleeding edge of the default branch instead of
the latest tagged version (which is the default behavior)
:param freeze: Set to True to store "frozen" hashes in rev instead of tag names.
Expand Down Expand Up @@ -145,7 +149,9 @@ def autoupdate_hooks(

subprocess_args.extend(repo_args)

completed_process = subprocess.run(subprocess_args, stdout=subprocess.PIPE)
completed_process = subprocess.run(
subprocess_args, stdout=subprocess.PIPE, cwd=folder_path
)
output = (
completed_process.stdout.decode("utf8") if completed_process.stdout else ""
)
Expand All @@ -154,14 +160,17 @@ def autoupdate_hooks(
else:
return ExecuteResult(successful=True, output=output)

def update(self) -> ExecuteResult:
def update(self, folder_path: Path) -> ExecuteResult:
"""
Installs the hooks defined in pre-commit-config.yml.
:param folder_path: Indicates the git folder against which you run secureli
:return: ExecuteResult, indicating success or failure.
"""
subprocess_args = ["pre-commit", "install-hooks", "--color", "always"]

completed_process = subprocess.run(subprocess_args, stdout=subprocess.PIPE)
completed_process = subprocess.run(
subprocess_args, stdout=subprocess.PIPE, cwd=folder_path
)
output = (
completed_process.stdout.decode("utf8") if completed_process.stdout else ""
)
Expand All @@ -170,16 +179,19 @@ def update(self) -> ExecuteResult:
else:
return ExecuteResult(successful=True, output=output)

def remove_unused_hooks(self) -> ExecuteResult:
def remove_unused_hooks(self, folder_path: Path) -> ExecuteResult:
"""
Removes unused hook repos from the cache. Pre-commit determines which flags are "unused" by comparing
the repos to the pre-commit-config.yaml file. Any cached hook repos that are not in the config file
will be removed from the cache.
:param folder_path: Indicates the git folder against which you run secureli
:return: ExecuteResult, indicating success or failure.
"""
subprocess_args = ["pre-commit", "gc", "--color", "always"]

completed_process = subprocess.run(subprocess_args, stdout=subprocess.PIPE)
completed_process = subprocess.run(
subprocess_args, stdout=subprocess.PIPE, cwd=folder_path
)
output = (
completed_process.stdout.decode("utf8") if completed_process.stdout else ""
)
Expand Down
4 changes: 2 additions & 2 deletions secureli/actions/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,15 @@ def _install_secureli(self, folder_path: Path, always_yes: bool) -> VerifyResult
self.action_deps.secureli_config.save(config)

# Create seCureLI pre-commit hook with invocation of `secureli scan`
self.action_deps.updater.pre_commit.install()
self.action_deps.updater.pre_commit.install(folder_path)

if secret_test_id := metadata.security_hook_id:
self.action_deps.echo.print(
f"{config.languages} supports secrets detection; running {secret_test_id}."
)

scan_result = self.action_deps.scanner.scan_repo(
ScanMode.ALL_FILES, specific_test=secret_test_id
folder_path, ScanMode.ALL_FILES, specific_test=secret_test_id
)

self.action_deps.echo.print(f"{scan_result.output}")
Expand Down
2 changes: 1 addition & 1 deletion secureli/actions/initializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def initialize_repo(self, folder_path: Path, reset: bool, always_yes: bool):
"""

# Will create a blank .secureli.yaml file if it does not exist
settings = self.action_deps.settings.load()
settings = self.action_deps.settings.load(folder_path)

# Why are we saving settings? CLI should not be modifying them....just reading
# With a templated example .secureli.yaml file, we won't be able to save
Expand Down
2 changes: 1 addition & 1 deletion secureli/actions/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def scan_repo(
if verify_result.outcome in self.halting_outcomes:
return

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

details = scan_result.output or "Unknown output during scan"
self.echo.print(details)
Expand Down
9 changes: 5 additions & 4 deletions secureli/actions/update.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import Optional

from pathlib import Path
from secureli.abstractions.echo import EchoAbstraction
from secureli.services.logging import LoggingService, LogAction
from secureli.services.updater import UpdaterService
Expand All @@ -19,16 +19,17 @@ def __init__(
self.logging = logging
self.updater = updater

def update_hooks(self, latest: Optional[bool] = False):
def update_hooks(self, folder_path: Path, latest: Optional[bool] = False):
"""
Installs the hooks defined in pre-commit-config.yml.
:param latest: Indicates whether you want to update to the latest versions
of the installed hooks.
:param folder_path: Indicates the git folder against which you run secureli
:return: ExecuteResult, indicating success or failure.
"""
if latest:
self.echo.print("Updating hooks to the latest version...")
update_result = self.updater.update_hooks()
update_result = self.updater.update_hooks(folder_path)
details = (
update_result.output
or "Unknown output while updating hooks to latest version"
Expand All @@ -42,7 +43,7 @@ def update_hooks(self, latest: Optional[bool] = False):
self.logging.success(LogAction.update)
else:
self.echo.print("Beginning update...")
install_result = self.updater.update()
install_result = self.updater.update(folder_path)
details = install_result.output or "Unknown output during hook installation"
self.echo.print(details)
if not install_result.successful:
Expand Down
41 changes: 36 additions & 5 deletions secureli/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from pathlib import Path
from typing import Optional

from typing_extensions import Annotated
import typer
from typer import Option

Expand All @@ -10,6 +10,7 @@
from secureli.abstractions.echo import Color
from secureli.resources import read_resource
from secureli.settings import Settings
import secureli.repositories.secureli_config as SecureliConfig

# Create SetupAction outside of DI, as it's not yet available.
setup_action = SetupAction(epilog_template_data=read_resource("epilog.md"))
Expand Down Expand Up @@ -48,11 +49,21 @@ def init(
"-y",
help="Say 'yes' to every prompt automatically without input",
),
directory: Annotated[
Optional[Path],
Option(
".",
"--directory",
"-d",
help="Run secureli against a specific directory",
),
] = ".",
):
"""
Detect languages and initialize pre-commit hooks and linters for the project
"""
container.initializer_action().initialize_repo(Path("."), reset, yes)
SecureliConfig.FOLDER_PATH = Path(directory)
container.initializer_action().initialize_repo(Path(directory), reset, yes)


@app.command()
Expand All @@ -75,11 +86,21 @@ def scan(
"-t",
help="Limit the scan to a specific hook ID from your pre-commit config",
),
directory: Annotated[
Optional[Path],
Option(
".",
"--directory",
"-d",
help="Run secureli against a specific directory",
),
] = ".",
):
"""
Performs an explicit check of the repository to detect security issues without remote logging.
"""
container.scan_action().scan_repo(Path("."), mode, yes, specific_test)
SecureliConfig.FOLDER_PATH = Path(directory)
container.scan_action().scan_repo(Path(directory), mode, yes, specific_test)


@app.command(hidden=True)
Expand All @@ -97,12 +118,22 @@ def update(
"--latest",
"-l",
help="Update the installed pre-commit hooks to their latest versions",
)
),
directory: Annotated[
Optional[Path],
Option(
".",
"--directory",
"-d",
help="Run secureli against a specific directory",
),
] = ".",
):
"""
Update linters, configuration, and all else needed to maintain a secure repository.
"""
container.update_action().update_hooks(latest)
SecureliConfig.FOLDER_PATH = Path(directory)
container.update_action().update_hooks(Path(directory), latest)


if __name__ == "__main__":
Expand Down
1 change: 1 addition & 0 deletions secureli/repositories/repo_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def list_repo_files(self, folder_path: Path) -> list[Path]:
:return: The visible files within the specified repo as a list of Path objects
"""
git_path = folder_path / ".git"

if not git_path.exists() or not git_path.is_dir():
raise ValueError("The current folder is not a Git repository!")

Expand Down
6 changes: 5 additions & 1 deletion secureli/repositories/secureli_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from pydantic import BaseModel

FOLDER_PATH = Path(".")


class SecureliConfig(BaseModel):
languages: Optional[list[str]]
Expand Down Expand Up @@ -46,6 +48,7 @@ def load(self) -> SecureliConfig:
"""
secureli_folder_path = self._initialize_secureli_directory()
secureli_config_path = secureli_folder_path / "repo-config.yaml"

if not secureli_config_path.exists():
return SecureliConfig()

Expand Down Expand Up @@ -101,6 +104,7 @@ def _initialize_secureli_directory(self):
Creates the .secureli folder within the current directory if needed.
:return: The folder path of the .secureli folder that either exists or was just created.
"""
secureli_folder_path = Path(".") / ".secureli"

secureli_folder_path = Path(FOLDER_PATH) / ".secureli"
secureli_folder_path.mkdir(parents=True, exist_ok=True)
return secureli_folder_path
3 changes: 2 additions & 1 deletion secureli/repositories/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,12 @@ def save(self, settings: SecureliFile):
with open(self.secureli_file_path, "w") as f:
yaml.dump(settings_dict, f)

def load(self) -> SecureliFile:
def load(self, folder_path: Path) -> SecureliFile:
"""
Reads the contents of the .secureli.yaml file and returns it
:return: SecureliFile containing the contents of the settings file
"""
self.secureli_file_path = folder_path / ".secureli.yaml"
if not self.secureli_file_path.exists():
return SecureliFile()

Expand Down
14 changes: 9 additions & 5 deletions secureli/services/git_ignore.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pathlib import Path

import pathspec
import secureli.repositories.secureli_config as SecureliConfig


class BadIgnoreBlockError(Exception):
Expand All @@ -17,18 +18,19 @@ class GitIgnoreService:
header = "# Secureli-generated files (do not modify):"
ignore_entries = [".secureli"]
footer = "# End Secureli-generated files"
git_ignore_path = Path("./.gitignore")

def ignore_secureli_files(self):
"""Creates a .gitignore, appends to an existing one, or updates the configuration"""
if not self.git_ignore_path.exists():
git_ignore_path = SecureliConfig.FOLDER_PATH / "./.gitignore"
if not git_ignore_path.exists():
# your repo doesn't have a gitignore? That's a bold move.
self._create_git_ignore()
else:
self._update_git_ignore()

def ignored_file_patterns(self) -> list[str]:
if not self.git_ignore_path.exists():
git_ignore_path = Path(SecureliConfig.FOLDER_PATH / "./.gitignore")
if not git_ignore_path.exists():
return []

"""Reads the lines from the .gitignore file"""
Expand Down Expand Up @@ -87,10 +89,12 @@ def _update_git_ignore(self):

def _write_file_contents(self, contents: str):
"""Update the .gitignore file with the provided contents"""
with open(self.git_ignore_path, "w") as f:
git_ignore_path = SecureliConfig.FOLDER_PATH / "./.gitignore"
with open(git_ignore_path, "w") as f:
f.write(contents)

def _read_file_contents(self) -> str:
"""Read the .gitignore file"""
with open(self.git_ignore_path, "r") as f:
git_ignore_path = SecureliConfig.FOLDER_PATH / "./.gitignore"
with open(git_ignore_path, "r") as f:
return f.read()
1 change: 1 addition & 0 deletions secureli/services/language_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from secureli.resources.slugify import slugify
from secureli.utilities.hash import hash_config
from secureli.utilities.patterns import combine_patterns
import secureli.repositories.secureli_config as SecureliConfig


class LanguageNotSupportedError(Exception):
Expand Down
Loading

0 comments on commit d23f94b

Please sign in to comment.