appsec-tool-kit 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/PKG-INFO +1 -1
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/appsec_kit/cli.py +5 -5
- appsec_tool_kit-0.2.0/appsec_kit/updater.py +74 -0
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/appsec_kit/versions.py +3 -3
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/pyproject.toml +2 -1
- appsec_tool_kit-0.2.0/renovate.json +35 -0
- appsec_tool_kit-0.2.0/tests/test_cli.py +61 -0
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/.github/workflows/tests.yml +0 -0
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/.gitignore +0 -0
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/.python-version +0 -0
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/README.md +0 -0
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/appsec_kit/__init__.py +0 -0
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/appsec_kit/detector.py +0 -0
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/appsec_kit/templates/__init__.py +0 -0
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/appsec_kit/templates/node_templates.py +0 -0
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/appsec_kit/templates/python_templates.py +0 -0
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/appsec_kit/writer.py +0 -0
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/tests/__init__.py +0 -0
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/tests/test_detector.py +0 -0
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/tests/test_templates.py +0 -0
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/tests/test_versions.py +0 -0
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/tests/test_writer.py +0 -0
- {appsec_tool_kit-0.1.0 → appsec_tool_kit-0.2.0}/uv.lock +0 -0
|
@@ -33,11 +33,11 @@ def _print_next_steps(lang: str, layers: list[str]) -> None:
|
|
|
33
33
|
lines: list[str] = []
|
|
34
34
|
|
|
35
35
|
if "precommit" in layers:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
36
|
+
pip_pkgs = "pre-commit detect-secrets" if "secrets" in layers else "pre-commit"
|
|
37
|
+
lines.append(f"pip install {pip_pkgs}")
|
|
38
|
+
if "secrets" in layers:
|
|
39
|
+
lines.append("detect-secrets scan > .secrets.baseline")
|
|
40
|
+
lines.append("pre-commit install")
|
|
41
41
|
|
|
42
42
|
if lang == "node" and "sast" in layers:
|
|
43
43
|
lines += [
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from urllib.error import URLError
|
|
5
|
+
from urllib.request import Request, urlopen
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
_VERSIONS_PATH = Path(__file__).parent / "versions.py"
|
|
12
|
+
|
|
13
|
+
# (github_repo, keep_v_prefix)
|
|
14
|
+
_TOOLS: dict[str, tuple[str, bool]] = {
|
|
15
|
+
"BANDIT": ("PyCQA/bandit", False),
|
|
16
|
+
"DETECT_SECRETS": ("Yelp/detect-secrets", True),
|
|
17
|
+
"PIP_AUDIT": ("pypa/pip-audit", True),
|
|
18
|
+
"GITLEAKS": ("gitleaks/gitleaks", True),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _fetch_latest_tag(repo: str) -> str | None:
|
|
23
|
+
url = f"https://api.github.com/repos/{repo}/releases/latest"
|
|
24
|
+
req = Request(url, headers={
|
|
25
|
+
"Accept": "application/vnd.github+json",
|
|
26
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
27
|
+
})
|
|
28
|
+
try:
|
|
29
|
+
with urlopen(req, timeout=10) as resp:
|
|
30
|
+
return json.loads(resp.read()).get("tag_name")
|
|
31
|
+
except (URLError, json.JSONDecodeError, KeyError):
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def bump_versions() -> None:
|
|
36
|
+
console.print()
|
|
37
|
+
console.print("[bold]Checking latest versions...[/bold]\n")
|
|
38
|
+
|
|
39
|
+
content = _VERSIONS_PATH.read_text()
|
|
40
|
+
updated = content
|
|
41
|
+
any_change = False
|
|
42
|
+
|
|
43
|
+
for var, (repo, keep_v) in _TOOLS.items():
|
|
44
|
+
tag = _fetch_latest_tag(repo)
|
|
45
|
+
if tag is None:
|
|
46
|
+
console.print(f" [yellow]⚠[/yellow] {var}: could not fetch from {repo}")
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
version = tag if keep_v else tag.lstrip("v")
|
|
50
|
+
|
|
51
|
+
match = re.search(rf'^{var} = "([^"]+)"', content, re.MULTILINE)
|
|
52
|
+
if not match:
|
|
53
|
+
console.print(f" [yellow]⚠[/yellow] {var}: not found in versions.py")
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
current = match.group(1)
|
|
57
|
+
if current == version:
|
|
58
|
+
console.print(f" [dim]–[/dim] {var}: [dim]{current} (up to date)[/dim]")
|
|
59
|
+
else:
|
|
60
|
+
updated = updated.replace(f'{var} = "{current}"', f'{var} = "{version}"')
|
|
61
|
+
console.print(f" [green]↑[/green] {var}: [dim]{current}[/dim] → [bold green]{version}[/bold green]")
|
|
62
|
+
any_change = True
|
|
63
|
+
|
|
64
|
+
console.print()
|
|
65
|
+
|
|
66
|
+
if any_change:
|
|
67
|
+
_VERSIONS_PATH.write_text(updated)
|
|
68
|
+
console.print("[green]versions.py updated.[/green] Commit when pronto:")
|
|
69
|
+
console.print(' [dim]git add appsec_kit/versions.py[/dim]')
|
|
70
|
+
console.print(' [dim]git commit -m "chore: bump pinned tool versions"[/dim]')
|
|
71
|
+
else:
|
|
72
|
+
console.print("[dim]All versions up to date.[/dim]")
|
|
73
|
+
|
|
74
|
+
console.print()
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "appsec-tool-kit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Interactive CLI to configure security tools for CI/CD pipelines"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -15,6 +15,7 @@ dependencies = [
|
|
|
15
15
|
|
|
16
16
|
[project.scripts]
|
|
17
17
|
appsec-kit = "appsec_kit.cli:run"
|
|
18
|
+
appsec-kit-bump = "appsec_kit.updater:bump_versions"
|
|
18
19
|
|
|
19
20
|
[tool.hatch.build.targets.wheel]
|
|
20
21
|
packages = ["appsec_kit"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
3
|
+
"extends": ["config:base"],
|
|
4
|
+
"customManagers": [
|
|
5
|
+
{
|
|
6
|
+
"customType": "regex",
|
|
7
|
+
"fileMatch": ["^appsec_kit/versions\\.py$"],
|
|
8
|
+
"matchStrings": ["BANDIT = \"(?<currentValue>[^\"]+)\""],
|
|
9
|
+
"depNameTemplate": "PyCQA/bandit",
|
|
10
|
+
"datasourceTemplate": "github-releases",
|
|
11
|
+
"extractVersionTemplate": "^v?(?<version>.+)$"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"customType": "regex",
|
|
15
|
+
"fileMatch": ["^appsec_kit/versions\\.py$"],
|
|
16
|
+
"matchStrings": ["DETECT_SECRETS = \"(?<currentValue>[^\"]+)\""],
|
|
17
|
+
"depNameTemplate": "Yelp/detect-secrets",
|
|
18
|
+
"datasourceTemplate": "github-releases"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"customType": "regex",
|
|
22
|
+
"fileMatch": ["^appsec_kit/versions\\.py$"],
|
|
23
|
+
"matchStrings": ["PIP_AUDIT = \"(?<currentValue>[^\"]+)\""],
|
|
24
|
+
"depNameTemplate": "pypa/pip-audit",
|
|
25
|
+
"datasourceTemplate": "github-releases"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"customType": "regex",
|
|
29
|
+
"fileMatch": ["^appsec_kit/versions\\.py$"],
|
|
30
|
+
"matchStrings": ["GITLEAKS = \"(?<currentValue>[^\"]+)\""],
|
|
31
|
+
"depNameTemplate": "gitleaks/gitleaks",
|
|
32
|
+
"datasourceTemplate": "github-releases"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from unittest.mock import patch
|
|
2
|
+
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
|
|
5
|
+
from appsec_kit.cli import _print_next_steps
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _capture(lang: str, layers: list[str]) -> str:
|
|
9
|
+
captured: list[str] = []
|
|
10
|
+
|
|
11
|
+
def _collect(*args, **_):
|
|
12
|
+
for arg in args:
|
|
13
|
+
if isinstance(arg, Panel):
|
|
14
|
+
captured.append(str(arg.renderable))
|
|
15
|
+
else:
|
|
16
|
+
captured.append(str(arg))
|
|
17
|
+
|
|
18
|
+
with patch("appsec_kit.cli.console") as mock_console:
|
|
19
|
+
mock_console.print.side_effect = _collect
|
|
20
|
+
_print_next_steps(lang, layers)
|
|
21
|
+
return "\n".join(captured)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestNextSteps:
|
|
25
|
+
def test_precommit_with_secrets_shows_baseline_and_detect_secrets(self):
|
|
26
|
+
out = _capture("python", ["precommit", "secrets"])
|
|
27
|
+
assert "detect-secrets scan" in out
|
|
28
|
+
assert "detect-secrets" in out
|
|
29
|
+
assert "pre-commit install" in out
|
|
30
|
+
|
|
31
|
+
def test_precommit_without_secrets_omits_baseline_and_detect_secrets_pkg(self):
|
|
32
|
+
out = _capture("python", ["precommit", "sast"])
|
|
33
|
+
assert "detect-secrets scan" not in out
|
|
34
|
+
assert "detect-secrets" not in out
|
|
35
|
+
assert "pre-commit install" in out
|
|
36
|
+
|
|
37
|
+
def test_precommit_only_omits_baseline(self):
|
|
38
|
+
out = _capture("python", ["precommit"])
|
|
39
|
+
assert "detect-secrets scan" not in out
|
|
40
|
+
assert "pre-commit install" in out
|
|
41
|
+
|
|
42
|
+
def test_no_precommit_shows_nothing(self):
|
|
43
|
+
out = _capture("python", ["sast", "deps", "secrets"])
|
|
44
|
+
assert "pre-commit install" not in out
|
|
45
|
+
assert "detect-secrets scan" not in out
|
|
46
|
+
|
|
47
|
+
def test_node_sast_shows_semgrep_token_instructions(self):
|
|
48
|
+
out = _capture("node", ["sast", "precommit"])
|
|
49
|
+
assert "SEMGREP_APP_TOKEN" in out
|
|
50
|
+
|
|
51
|
+
def test_python_sast_omits_semgrep_token_instructions(self):
|
|
52
|
+
out = _capture("python", ["sast", "precommit"])
|
|
53
|
+
assert "SEMGREP_APP_TOKEN" not in out
|
|
54
|
+
|
|
55
|
+
def test_ci_layers_show_git_push_instructions(self):
|
|
56
|
+
out = _capture("python", ["sast"])
|
|
57
|
+
assert "git push" in out
|
|
58
|
+
|
|
59
|
+
def test_no_layers_shows_nothing(self):
|
|
60
|
+
out = _capture("python", [])
|
|
61
|
+
assert out == ""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|