issue 77 pt3 golangci

This commit is contained in:
CarterPerez-dev 2026-02-18 20:08:20 -05:00
parent 1316d31129
commit 7168174006
17 changed files with 330 additions and 179 deletions

View File

@ -87,9 +87,12 @@ jobs:
type: biome type: biome
path: TEMPLATES/fullstack-template/frontend path: TEMPLATES/fullstack-template/frontend
# Go # Go
- name: simple-vulnerability-scanner
type: go
path: PROJECTS/beginner/simple-vulnerability-scanner
- name: docker-security-audit - name: docker-security-audit
type: go type: go
path: PROJECTS/beginner/docker-security-audit path: PROJECTS/intermediate/docker-security-audit
defaults: defaults:
run: run:
@ -142,9 +145,13 @@ jobs:
if: matrix.type == 'go' if: matrix.type == 'go'
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.21' go-version-file: ${{ matrix.path }}/go.mod
cache-dependency-path: ${{ matrix.path }}/go.sum cache-dependency-path: ${{ matrix.path }}/go.sum
- name: Install golangci-lint
if: matrix.type == 'go'
run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
# Ruff Linting # Ruff Linting
- name: Run ruff - name: Run ruff
if: matrix.type == 'ruff' if: matrix.type == 'ruff'
@ -183,7 +190,7 @@ jobs:
id: golangci id: golangci
run: | run: |
echo "Running golangci-lint..." echo "Running golangci-lint..."
if golangci-lint run --out-format=colored-line-number > golangci-output.txt 2>&1; then if golangci-lint run > golangci-output.txt 2>&1; then
echo "GOLANGCI_PASSED=true" >> $GITHUB_ENV echo "GOLANGCI_PASSED=true" >> $GITHUB_ENV
echo "No golangci-lint errors found!" echo "No golangci-lint errors found!"
else else

View File

@ -92,8 +92,24 @@ repos:
exclude: (\.venv|__pycache__|\.pytest_cache)/ exclude: (\.venv|__pycache__|\.pytest_cache)/
# TODO: add golangci checks for GO projects # Go golangci-lint Checks
- repo: local
hooks:
- id: golangci-lint-simple-vulnerability-scanner
name: golangci-lint (simple-vulnerability-scanner)
entry: bash -c 'cd PROJECTS/beginner/simple-vulnerability-scanner && golangci-lint run --fix'
language: system
files: ^PROJECTS/beginner/simple-vulnerability-scanner/
types: [go]
pass_filenames: false
- id: golangci-lint-docker-security-audit
name: golangci-lint (docker-security-audit)
entry: bash -c 'cd PROJECTS/intermediate/docker-security-audit && golangci-lint run --fix'
language: system
files: ^PROJECTS/intermediate/docker-security-audit/
types: [go]
pass_filenames: false
# Biome Frontend Checks # Biome Frontend Checks
- repo: local - repo: local

View File

@ -37,14 +37,14 @@ from base64_tool.utils import (
app = typer.Typer( app = typer.Typer(
name="b64tool", name = "b64tool",
help=("Multi-format encoding/decoding CLI " help = ("Multi-format encoding/decoding CLI "
"with recursive layer detection"), "with recursive layer detection"),
no_args_is_help=True, no_args_is_help = True,
pretty_exceptions_show_locals=False, pretty_exceptions_show_locals = False,
) )
_console = Console(stderr=True) _console = Console(stderr = True)
def _version_callback(value: bool) -> None: def _version_callback(value: bool) -> None:
@ -60,27 +60,27 @@ def main(
typer.Option( typer.Option(
"--version", "--version",
"-v", "-v",
help="Show version and exit.", help = "Show version and exit.",
callback=_version_callback, callback = _version_callback,
is_eager=True, is_eager = True,
), ),
] = False, ] = False,
) -> None: ) -> None:
pass pass
@app.command(name="encode") @app.command(name = "encode")
def encode_cmd( def encode_cmd(
data: Annotated[ data: Annotated[
str | None, str | None,
typer.Argument(help="Data to encode."), typer.Argument(help = "Data to encode."),
] = None, ] = None,
fmt: Annotated[ fmt: Annotated[
EncodingFormat, EncodingFormat,
typer.Option( typer.Option(
"--format", "--format",
"-f", "-f",
help="Target encoding format.", help = "Target encoding format.",
), ),
] = EncodingFormat.BASE64, ] = EncodingFormat.BASE64,
file: Annotated[ file: Annotated[
@ -88,21 +88,21 @@ def encode_cmd(
typer.Option( typer.Option(
"--file", "--file",
"-i", "-i",
help="Read input from file.", help = "Read input from file.",
), ),
] = None, ] = None,
form: Annotated[ form: Annotated[
bool, bool,
typer.Option( typer.Option(
"--form", "--form",
help="Use form-encoding for URL (space becomes +).", help = "Use form-encoding for URL (space becomes +).",
), ),
] = False, ] = False,
) -> None: ) -> None:
try: try:
raw = resolve_input_bytes(data, file) raw = resolve_input_bytes(data, file)
if fmt == EncodingFormat.URL and form: if fmt == EncodingFormat.URL and form:
result = encode_url(raw, form=True) result = encode_url(raw, form = True)
else: else:
result = encode(raw, fmt) result = encode(raw, fmt)
print_encoded(result, fmt) print_encoded(result, fmt)
@ -110,21 +110,21 @@ def encode_cmd(
raise raise
except Exception as exc: except Exception as exc:
_console.print(f"[red]Error:[/red] {exc}") _console.print(f"[red]Error:[/red] {exc}")
raise typer.Exit(code=ExitCode.ERROR) from None raise typer.Exit(code = ExitCode.ERROR) from None
@app.command(name="decode") @app.command(name = "decode")
def decode_cmd( def decode_cmd(
data: Annotated[ data: Annotated[
str | None, str | None,
typer.Argument(help="Data to decode."), typer.Argument(help = "Data to decode."),
] = None, ] = None,
fmt: Annotated[ fmt: Annotated[
EncodingFormat, EncodingFormat,
typer.Option( typer.Option(
"--format", "--format",
"-f", "-f",
help="Source encoding format.", help = "Source encoding format.",
), ),
] = EncodingFormat.BASE64, ] = EncodingFormat.BASE64,
file: Annotated[ file: Annotated[
@ -132,21 +132,21 @@ def decode_cmd(
typer.Option( typer.Option(
"--file", "--file",
"-i", "-i",
help="Read input from file.", help = "Read input from file.",
), ),
] = None, ] = None,
form: Annotated[ form: Annotated[
bool, bool,
typer.Option( typer.Option(
"--form", "--form",
help="Use form-decoding for URL (+ becomes space).", help = "Use form-decoding for URL (+ becomes space).",
), ),
] = False, ] = False,
) -> None: ) -> None:
try: try:
text = resolve_input_text(data, file) text = resolve_input_text(data, file)
if fmt == EncodingFormat.URL and form: if fmt == EncodingFormat.URL and form:
result = decode_url(text, form=True) result = decode_url(text, form = True)
else: else:
result = decode(text, fmt) result = decode(text, fmt)
print_decoded(result) print_decoded(result)
@ -154,21 +154,21 @@ def decode_cmd(
raise raise
except Exception as exc: except Exception as exc:
_console.print(f"[red]Error:[/red] {exc}") _console.print(f"[red]Error:[/red] {exc}")
raise typer.Exit(code=ExitCode.ERROR) from None raise typer.Exit(code = ExitCode.ERROR) from None
@app.command(name="detect") @app.command(name = "detect")
def detect_cmd( def detect_cmd(
data: Annotated[ data: Annotated[
str | None, str | None,
typer.Argument(help="Data to analyze."), typer.Argument(help = "Data to analyze."),
] = None, ] = None,
file: Annotated[ file: Annotated[
Path | None, Path | None,
typer.Option( typer.Option(
"--file", "--file",
"-i", "-i",
help="Read input from file.", help = "Read input from file.",
), ),
] = None, ] = None,
verbose: Annotated[ verbose: Annotated[
@ -176,7 +176,7 @@ def detect_cmd(
typer.Option( typer.Option(
"--verbose", "--verbose",
"-V", "-V",
help="Show per-format score breakdown.", help = "Show per-format score breakdown.",
), ),
] = False, ] = False,
) -> None: ) -> None:
@ -184,26 +184,26 @@ def detect_cmd(
text = resolve_input_text(data, file) text = resolve_input_text(data, file)
results = detect_encoding(text) results = detect_encoding(text)
scores = score_all_formats(text) if verbose else None scores = score_all_formats(text) if verbose else None
print_detection(results, verbose_scores=scores) print_detection(results, verbose_scores = scores)
except typer.BadParameter: except typer.BadParameter:
raise raise
except Exception as exc: except Exception as exc:
_console.print(f"[red]Error:[/red] {exc}") _console.print(f"[red]Error:[/red] {exc}")
raise typer.Exit(code=ExitCode.ERROR) from None raise typer.Exit(code = ExitCode.ERROR) from None
@app.command(name="peel") @app.command(name = "peel")
def peel_cmd( def peel_cmd(
data: Annotated[ data: Annotated[
str | None, str | None,
typer.Argument(help="Data to recursively decode."), typer.Argument(help = "Data to recursively decode."),
] = None, ] = None,
file: Annotated[ file: Annotated[
Path | None, Path | None,
typer.Option( typer.Option(
"--file", "--file",
"-i", "-i",
help="Read input from file.", help = "Read input from file.",
), ),
] = None, ] = None,
max_depth: Annotated[ max_depth: Annotated[
@ -211,7 +211,7 @@ def peel_cmd(
typer.Option( typer.Option(
"--max-depth", "--max-depth",
"-d", "-d",
help="Maximum decoding layers.", help = "Maximum decoding layers.",
), ),
] = PEEL_MAX_DEPTH, ] = PEEL_MAX_DEPTH,
verbose: Annotated[ verbose: Annotated[
@ -219,34 +219,34 @@ def peel_cmd(
typer.Option( typer.Option(
"--verbose", "--verbose",
"-V", "-V",
help="Show per-format score breakdown at each layer.", help = "Show per-format score breakdown at each layer.",
), ),
] = False, ] = False,
) -> None: ) -> None:
try: try:
text = resolve_input_text(data, file) text = resolve_input_text(data, file)
result = peel(text, max_depth=max_depth, verbose=verbose) result = peel(text, max_depth = max_depth, verbose = verbose)
print_peel_result(result, verbose=verbose) print_peel_result(result, verbose = verbose)
except typer.BadParameter: except typer.BadParameter:
raise raise
except Exception as exc: except Exception as exc:
_console.print(f"[red]Error:[/red] {exc}") _console.print(f"[red]Error:[/red] {exc}")
raise typer.Exit(code=ExitCode.ERROR) from None raise typer.Exit(code = ExitCode.ERROR) from None
@app.command(name="chain") @app.command(name = "chain")
def chain_cmd( def chain_cmd(
data: Annotated[ data: Annotated[
str | None, str | None,
typer.Argument(help="Data to encode through chain."), typer.Argument(help = "Data to encode through chain."),
] = None, ] = None,
steps: Annotated[ steps: Annotated[
str, str,
typer.Option( typer.Option(
"--steps", "--steps",
"-s", "-s",
help=("Comma-separated encoding formats " help = ("Comma-separated encoding formats "
"(e.g. base64,hex,url)."), "(e.g. base64,hex,url)."),
), ),
] = "base64", ] = "base64",
file: Annotated[ file: Annotated[
@ -254,7 +254,7 @@ def chain_cmd(
typer.Option( typer.Option(
"--file", "--file",
"-i", "-i",
help="Read input from file.", help = "Read input from file.",
), ),
] = None, ] = None,
) -> None: ) -> None:
@ -275,7 +275,7 @@ def chain_cmd(
raise raise
except Exception as exc: except Exception as exc:
_console.print(f"[red]Error:[/red] {exc}") _console.print(f"[red]Error:[/red] {exc}")
raise typer.Exit(code=ExitCode.ERROR) from None raise typer.Exit(code = ExitCode.ERROR) from None
def _parse_chain_steps(raw: str) -> list[EncodingFormat]: def _parse_chain_steps(raw: str) -> list[EncodingFormat]:

View File

@ -62,6 +62,7 @@ class ScoreWeight:
URL_RATIO_CAP: Final[float] = 0.35 URL_RATIO_CAP: Final[float] = 0.35
URL_DECODE_CHANGED: Final[float] = 0.15 URL_DECODE_CHANGED: Final[float] = 0.15
BASE64_CHARSET: Final[ BASE64_CHARSET: Final[
frozenset[str] frozenset[str]
] = frozenset("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=") ] = frozenset("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=")
@ -75,4 +76,3 @@ BASE32_CHARSET: Final[frozenset[str]] = frozenset("ABCDEFGHIJKLMNOPQRSTUVWXYZ234
HEX_CHARSET: Final[frozenset[str]] = frozenset("0123456789abcdefABCDEF") HEX_CHARSET: Final[frozenset[str]] = frozenset("0123456789abcdefABCDEF")
HEX_SEPARATORS: Final[frozenset[str]] = frozenset(" :.-") HEX_SEPARATORS: Final[frozenset[str]] = frozenset(" :.-")

View File

@ -22,7 +22,7 @@ from base64_tool.encoders import try_decode
from base64_tool.utils import is_printable_text from base64_tool.utils import is_printable_text
@dataclass(frozen=True, slots=True) @dataclass(frozen = True, slots = True)
class DetectionResult: class DetectionResult:
format: EncodingFormat format: EncodingFormat
confidence: float confidence: float
@ -152,9 +152,7 @@ def _score_hex(data: str) -> float:
else: else:
score -= W.HEX_NO_ALPHA_PENALTY score -= W.HEX_NO_ALPHA_PENALTY
is_consistent_case = ( is_consistent_case = (hex_only == hex_only.lower() or hex_only == hex_only.upper())
hex_only == hex_only.lower() or hex_only == hex_only.upper()
)
if is_consistent_case: if is_consistent_case:
score += W.HEX_CONSISTENT_CASE score += W.HEX_CONSISTENT_CASE
@ -188,20 +186,22 @@ def _score_url(data: str) -> float:
decoded = try_decode(data, EncodingFormat.URL) decoded = try_decode(data, EncodingFormat.URL)
if decoded is not None: if decoded is not None:
decoded_text = decoded.decode("utf-8", errors="replace") decoded_text = decoded.decode("utf-8", errors = "replace")
if decoded_text != data: if decoded_text != data:
score += W.URL_DECODE_CHANGED score += W.URL_DECODE_CHANGED
return min(score, 1.0) return min(score, 1.0)
_SCORERS: dict[EncodingFormat, Callable[[str], float]] = { _SCORERS: dict[EncodingFormat,
EncodingFormat.BASE64: _score_base64, Callable[[str],
EncodingFormat.BASE64URL: _score_base64url, float]] = {
EncodingFormat.BASE32: _score_base32, EncodingFormat.BASE64: _score_base64,
EncodingFormat.HEX: _score_hex, EncodingFormat.BASE64URL: _score_base64url,
EncodingFormat.URL: _score_url, EncodingFormat.BASE32: _score_base32,
} EncodingFormat.HEX: _score_hex,
EncodingFormat.URL: _score_url,
}
def score_all_formats(data: str) -> dict[EncodingFormat, float]: def score_all_formats(data: str) -> dict[EncodingFormat, float]:
@ -216,13 +216,14 @@ def detect_encoding(data: str) -> list[DetectionResult]:
decoded = try_decode(data, fmt) decoded = try_decode(data, fmt)
results.append( results.append(
DetectionResult( DetectionResult(
format=fmt, format = fmt,
confidence=round(confidence, 2), confidence = round(confidence,
decoded=decoded, 2),
decoded = decoded,
) )
) )
results.sort(key=lambda r: r.confidence, reverse=True) results.sort(key = lambda r: r.confidence, reverse = True)
return results return results

View File

@ -20,7 +20,7 @@ from base64_tool.peeler import PeelResult
from base64_tool.utils import safe_bytes_preview from base64_tool.utils import safe_bytes_preview
console = Console(stderr=True) console = Console(stderr = True)
def is_piped() -> bool: def is_piped() -> bool:
@ -37,44 +37,49 @@ def print_encoded(result: str, fmt: EncodingFormat) -> None:
write_raw(result) write_raw(result)
return return
panel = Panel( panel = Panel(
Text(result, style="green"), Text(result,
title=f"[bold cyan]{fmt.value}[/bold cyan] encoded", style = "green"),
border_style="cyan", title = f"[bold cyan]{fmt.value}[/bold cyan] encoded",
border_style = "cyan",
) )
console.print(panel) console.print(panel)
def print_decoded(result: bytes) -> None: def print_decoded(result: bytes) -> None:
preview = safe_bytes_preview(result, length=4096) preview = safe_bytes_preview(result, length = 4096)
if is_piped(): if is_piped():
write_raw(preview) write_raw(preview)
return return
panel = Panel( panel = Panel(
Text(preview, style="green"), Text(preview,
title="[bold cyan]Decoded[/bold cyan]", style = "green"),
border_style="cyan", title = "[bold cyan]Decoded[/bold cyan]",
border_style = "cyan",
) )
console.print(panel) console.print(panel)
def print_score_breakdown( def print_score_breakdown(
scores: dict[EncodingFormat, float], scores: dict[EncodingFormat,
float],
) -> None: ) -> None:
table = Table( table = Table(
title="Score Breakdown", title = "Score Breakdown",
show_header=True, show_header = True,
header_style="bold magenta", header_style = "bold magenta",
) )
table.add_column("Format", style="cyan", min_width=10) table.add_column("Format", style = "cyan", min_width = 10)
table.add_column( table.add_column(
"Score", "Score",
justify="right", justify = "right",
min_width=8, min_width = 8,
) )
table.add_column("Status", min_width=10) table.add_column("Status", min_width = 10)
sorted_scores = sorted( sorted_scores = sorted(
scores.items(), key=lambda x: x[1], reverse=True, scores.items(),
key = lambda x: x[1],
reverse = True,
) )
for fmt, score in sorted_scores: for fmt, score in sorted_scores:
color = _confidence_color(score) color = _confidence_color(score)
@ -96,7 +101,8 @@ def print_score_breakdown(
def print_detection( def print_detection(
results: list[DetectionResult], results: list[DetectionResult],
*, *,
verbose_scores: dict[EncodingFormat, float] | None = None, verbose_scores: dict[EncodingFormat,
float] | None = None,
) -> None: ) -> None:
if verbose_scores is not None: if verbose_scores is not None:
print_score_breakdown(verbose_scores) print_score_breakdown(verbose_scores)
@ -107,18 +113,18 @@ def print_detection(
return return
table = Table( table = Table(
title="Detection Results", title = "Detection Results",
show_header=True, show_header = True,
header_style="bold magenta", header_style = "bold magenta",
) )
table.add_column("Format", style="cyan", min_width=10) table.add_column("Format", style = "cyan", min_width = 10)
table.add_column( table.add_column(
"Confidence", "Confidence",
justify="right", justify = "right",
style="green", style = "green",
min_width=12, min_width = 12,
) )
table.add_column("Decoded Preview", style="dim") table.add_column("Decoded Preview", style = "dim")
for result in results: for result in results:
confidence_str = f"{result.confidence:.0%}" confidence_str = f"{result.confidence:.0%}"
@ -171,18 +177,20 @@ def print_peel_result(
console.print() console.print()
preview = safe_bytes_preview(result.final_output, length=4096) preview = safe_bytes_preview(result.final_output, length = 4096)
panel = Panel( panel = Panel(
Text(preview, style="bold green"), Text(preview,
title="[bold]Final Output[/bold]", style = "bold green"),
border_style="green", title = "[bold]Final Output[/bold]",
subtitle=(f"[dim]{layer_count} layer{suffix} peeled[/dim]"), border_style = "green",
subtitle = (f"[dim]{layer_count} layer{suffix} peeled[/dim]"),
) )
console.print(panel) console.print(panel)
def print_chain_result( def print_chain_result(
steps: list[tuple[EncodingFormat, str]], steps: list[tuple[EncodingFormat,
str]],
final: str, final: str,
) -> None: ) -> None:
if is_piped(): if is_piped():
@ -196,7 +204,7 @@ def print_chain_result(
for i, (fmt, intermediate) in enumerate(steps): for i, (fmt, intermediate) in enumerate(steps):
marker = "start" if i == 0 else "step" marker = "start" if i == 0 else "step"
arrow = f" [{marker}] " if i == 0 else " -> " arrow = f" [{marker}] " if i == 0 else " -> "
truncated = intermediate[:PREVIEW_LENGTH] truncated = intermediate[: PREVIEW_LENGTH]
ellipsis = "..." if len(intermediate) > PREVIEW_LENGTH else "" ellipsis = "..." if len(intermediate) > PREVIEW_LENGTH else ""
console.print( console.print(
f"{arrow}[cyan]{fmt.value}[/cyan] " f"{arrow}[cyan]{fmt.value}[/cyan] "
@ -205,10 +213,11 @@ def print_chain_result(
console.print() console.print()
panel = Panel( panel = Panel(
Text(final, style="green"), Text(final,
title="[bold]Chain Result[/bold]", style = "green"),
border_style="cyan", title = "[bold]Chain Result[/bold]",
subtitle=f"[dim]{len(steps)} steps[/dim]", border_style = "cyan",
subtitle = f"[dim]{len(steps)} steps[/dim]",
) )
console.print(panel) console.print(panel)

View File

@ -14,7 +14,7 @@ from base64_tool.detector import detect_best, score_all_formats
from base64_tool.utils import safe_bytes_preview, truncate from base64_tool.utils import safe_bytes_preview, truncate
@dataclass(frozen=True, slots=True) @dataclass(frozen = True, slots = True)
class PeelLayer: class PeelLayer:
depth: int depth: int
format: EncodingFormat format: EncodingFormat
@ -24,7 +24,7 @@ class PeelLayer:
all_scores: tuple[tuple[EncodingFormat, float], ...] = () all_scores: tuple[tuple[EncodingFormat, float], ...] = ()
@dataclass(frozen=True, slots=True) @dataclass(frozen = True, slots = True)
class PeelResult: class PeelResult:
layers: tuple[PeelLayer, ...] layers: tuple[PeelLayer, ...]
final_output: bytes final_output: bytes
@ -52,20 +52,16 @@ def peel(
if detection.decoded is None: if detection.decoded is None:
break break
scores = ( scores = (tuple(score_all_formats(current_text).items()) if verbose else ())
tuple(score_all_formats(current_text).items())
if verbose
else ()
)
decoded_bytes = detection.decoded decoded_bytes = detection.decoded
layer = PeelLayer( layer = PeelLayer(
depth=depth + 1, depth = depth + 1,
format=detection.format, format = detection.format,
confidence=detection.confidence, confidence = detection.confidence,
encoded_preview=truncate(current_text), encoded_preview = truncate(current_text),
decoded_preview=safe_bytes_preview(decoded_bytes), decoded_preview = safe_bytes_preview(decoded_bytes),
all_scores=scores, all_scores = scores,
) )
layers.append(layer) layers.append(layer)
current_bytes = decoded_bytes current_bytes = decoded_bytes
@ -76,7 +72,7 @@ def peel(
break break
return PeelResult( return PeelResult(
layers=tuple(layers), layers = tuple(layers),
final_output=current_bytes, final_output = current_bytes,
success=len(layers) > 0, success = len(layers) > 0,
) )

View File

@ -26,5 +26,6 @@
from caesar_cipher.cipher import CaesarCipher from caesar_cipher.cipher import CaesarCipher
from caesar_cipher.analyzer import FrequencyAnalyzer from caesar_cipher.analyzer import FrequencyAnalyzer
__version__ = "0.1.0" __version__ = "0.1.0"
__all__ = ["CaesarCipher", "FrequencyAnalyzer"] __all__ = ["CaesarCipher", "FrequencyAnalyzer"]

View File

@ -24,7 +24,6 @@ from dnslookup.resolver import (
console = Console() console = Console()
RECORD_COLORS: dict[RecordType, RECORD_COLORS: dict[RecordType,
str] = { str] = {
RecordType.A: "green", RecordType.A: "green",

View File

@ -134,11 +134,11 @@ func runInit() error {
name := filepath.Base(dir) name := filepath.Base(dir)
content := fmt.Sprintf(pyprojectTemplate, name) content := fmt.Sprintf(pyprojectTemplate, name)
if err := os.WriteFile( if err := os.WriteFile( //nolint:gosec // pyproject.toml needs 0644 for uv/pip reads
"pyproject.toml", "pyproject.toml",
[]byte(content), []byte(content),
0o644, 0o644,
); err != nil { //nolint:gosec ); err != nil {
return fmt.Errorf("write pyproject.toml: %w", err) return fmt.Errorf("write pyproject.toml: %w", err)
} }

View File

@ -1,5 +1,5 @@
/* /*
AngelaMos | 2026 © AngelaMos | 2026
container.go container.go
*/ */
@ -13,7 +13,7 @@ import (
"github.com/CarterPerez-dev/docksec/internal/docker" "github.com/CarterPerez-dev/docksec/internal/docker"
"github.com/CarterPerez-dev/docksec/internal/finding" "github.com/CarterPerez-dev/docksec/internal/finding"
"github.com/CarterPerez-dev/docksec/internal/rules" "github.com/CarterPerez-dev/docksec/internal/rules"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container"
) )
type ContainerAnalyzer struct { type ContainerAnalyzer struct {
@ -49,7 +49,7 @@ func (a *ContainerAnalyzer) Analyze(
} }
func (a *ContainerAnalyzer) analyzeContainer( func (a *ContainerAnalyzer) analyzeContainer(
info types.ContainerJSON, info container.InspectResponse,
) finding.Collection { ) finding.Collection {
var findings finding.Collection var findings finding.Collection
target := finding.Target{ target := finding.Target{
@ -75,7 +75,7 @@ func (a *ContainerAnalyzer) analyzeContainer(
func (a *ContainerAnalyzer) checkPrivileged( func (a *ContainerAnalyzer) checkPrivileged(
target finding.Target, target finding.Target,
info types.ContainerJSON, info container.InspectResponse,
) finding.Collection { ) finding.Collection {
var findings finding.Collection var findings finding.Collection
@ -95,7 +95,7 @@ func (a *ContainerAnalyzer) checkPrivileged(
func (a *ContainerAnalyzer) checkCapabilities( func (a *ContainerAnalyzer) checkCapabilities(
target finding.Target, target finding.Target,
info types.ContainerJSON, info container.InspectResponse,
) finding.Collection { ) finding.Collection {
var findings finding.Collection var findings finding.Collection
@ -127,7 +127,7 @@ func (a *ContainerAnalyzer) checkCapabilities(
func (a *ContainerAnalyzer) checkMounts( func (a *ContainerAnalyzer) checkMounts(
target finding.Target, target finding.Target,
info types.ContainerJSON, info container.InspectResponse,
) finding.Collection { ) finding.Collection {
var findings finding.Collection var findings finding.Collection
@ -172,7 +172,7 @@ func (a *ContainerAnalyzer) checkMounts(
func (a *ContainerAnalyzer) checkNamespaces( func (a *ContainerAnalyzer) checkNamespaces(
target finding.Target, target finding.Target,
info types.ContainerJSON, info container.InspectResponse,
) finding.Collection { ) finding.Collection {
var findings finding.Collection var findings finding.Collection
@ -225,7 +225,7 @@ func (a *ContainerAnalyzer) checkNamespaces(
func (a *ContainerAnalyzer) checkSecurityOptions( func (a *ContainerAnalyzer) checkSecurityOptions(
target finding.Target, target finding.Target,
info types.ContainerJSON, info container.InspectResponse,
) finding.Collection { ) finding.Collection {
var findings finding.Collection var findings finding.Collection
@ -298,7 +298,7 @@ func (a *ContainerAnalyzer) checkSecurityOptions(
func (a *ContainerAnalyzer) checkResourceLimits( func (a *ContainerAnalyzer) checkResourceLimits(
target finding.Target, target finding.Target,
info types.ContainerJSON, info container.InspectResponse,
) finding.Collection { ) finding.Collection {
var findings finding.Collection var findings finding.Collection
@ -342,7 +342,7 @@ func (a *ContainerAnalyzer) checkResourceLimits(
func (a *ContainerAnalyzer) checkReadonlyRootfs( func (a *ContainerAnalyzer) checkReadonlyRootfs(
target finding.Target, target finding.Target,
info types.ContainerJSON, info container.InspectResponse,
) finding.Collection { ) finding.Collection {
var findings finding.Collection var findings finding.Collection

View File

@ -1,5 +1,5 @@
/* /*
AngelaMos | 2026 © AngelaMos | 2026
container_test.go container_test.go
*/ */
@ -12,23 +12,33 @@ import (
"testing" "testing"
"github.com/CarterPerez-dev/docksec/internal/finding" "github.com/CarterPerez-dev/docksec/internal/finding"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func loadContainerJSON(t *testing.T, filename string) types.ContainerJSON { func loadContainerJSON(
t *testing.T,
filename string,
) container.InspectResponse {
t.Helper() t.Helper()
path := filepath.Join("..", "..", "tests", "testdata", "containers", filename) path := filepath.Join(
"..",
"..",
"tests",
"testdata",
"containers",
filename,
)
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
require.NoError(t, err, "Failed to read container JSON file") require.NoError(t, err, "Failed to read container JSON file")
var container types.ContainerJSON var ctr container.InspectResponse
err = json.Unmarshal(data, &container) err = json.Unmarshal(data, &ctr)
require.NoError(t, err, "Failed to unmarshal container JSON") require.NoError(t, err, "Failed to unmarshal container JSON")
return container return ctr
} }
func TestContainerAnalyzer_PrivilegedContainer(t *testing.T) { func TestContainerAnalyzer_PrivilegedContainer(t *testing.T) {

View File

@ -1,5 +1,5 @@
/* /*
AngelaMos | 2026 © AngelaMos | 2026
image.go image.go
*/ */
@ -13,7 +13,7 @@ import (
"github.com/CarterPerez-dev/docksec/internal/benchmark" "github.com/CarterPerez-dev/docksec/internal/benchmark"
"github.com/CarterPerez-dev/docksec/internal/docker" "github.com/CarterPerez-dev/docksec/internal/docker"
"github.com/CarterPerez-dev/docksec/internal/finding" "github.com/CarterPerez-dev/docksec/internal/finding"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/image"
) )
type ImageAnalyzer struct { type ImageAnalyzer struct {
@ -62,7 +62,7 @@ func (a *ImageAnalyzer) Analyze(
func (a *ImageAnalyzer) analyzeImage( func (a *ImageAnalyzer) analyzeImage(
target finding.Target, target finding.Target,
info types.ImageInspect, info image.InspectResponse,
) finding.Collection { ) finding.Collection {
var findings finding.Collection var findings finding.Collection
@ -75,7 +75,7 @@ func (a *ImageAnalyzer) analyzeImage(
func (a *ImageAnalyzer) checkRootUser( func (a *ImageAnalyzer) checkRootUser(
target finding.Target, target finding.Target,
info types.ImageInspect, info image.InspectResponse,
) finding.Collection { ) finding.Collection {
var findings finding.Collection var findings finding.Collection
@ -100,7 +100,7 @@ func (a *ImageAnalyzer) checkRootUser(
func (a *ImageAnalyzer) checkHealthcheck( func (a *ImageAnalyzer) checkHealthcheck(
target finding.Target, target finding.Target,
info types.ImageInspect, info image.InspectResponse,
) finding.Collection { ) finding.Collection {
var findings finding.Collection var findings finding.Collection
@ -139,7 +139,7 @@ func (a *ImageAnalyzer) checkHealthcheck(
func (a *ImageAnalyzer) checkExposedPorts( func (a *ImageAnalyzer) checkExposedPorts(
target finding.Target, target finding.Target,
info types.ImageInspect, info image.InspectResponse,
) finding.Collection { ) finding.Collection {
var findings finding.Collection var findings finding.Collection

View File

@ -1,5 +1,5 @@
/* /*
CarterPerez-dev | 2026 © AngelaMos | 2026
client.go client.go
*/ */
@ -90,7 +90,7 @@ func (c *Client) ServerVersion(ctx context.Context) (types.Version, error) {
func (c *Client) ListContainers( func (c *Client) ListContainers(
ctx context.Context, ctx context.Context,
all bool, all bool,
) ([]types.Container, error) { ) ([]container.Summary, error) {
listCtx, cancel := context.WithTimeout(ctx, config.DefaultTimeout) listCtx, cancel := context.WithTimeout(ctx, config.DefaultTimeout)
defer cancel() defer cancel()
@ -107,13 +107,13 @@ func (c *Client) ListContainers(
func (c *Client) InspectContainer( func (c *Client) InspectContainer(
ctx context.Context, ctx context.Context,
containerID string, containerID string,
) (types.ContainerJSON, error) { ) (container.InspectResponse, error) {
inspectCtx, cancel := context.WithTimeout(ctx, config.InspectTimeout) inspectCtx, cancel := context.WithTimeout(ctx, config.InspectTimeout)
defer cancel() defer cancel()
info, err := c.api.ContainerInspect(inspectCtx, containerID) info, err := c.api.ContainerInspect(inspectCtx, containerID)
if err != nil { if err != nil {
return types.ContainerJSON{}, fmt.Errorf( return container.InspectResponse{}, fmt.Errorf(
"inspecting container %s: %w", "inspecting container %s: %w",
containerID, containerID,
err, err,
@ -136,13 +136,13 @@ func (c *Client) ListImages(ctx context.Context) ([]image.Summary, error) {
func (c *Client) InspectImage( func (c *Client) InspectImage(
ctx context.Context, ctx context.Context,
imageID string, imageID string,
) (types.ImageInspect, error) { ) (image.InspectResponse, error) {
inspectCtx, cancel := context.WithTimeout(ctx, config.InspectTimeout) inspectCtx, cancel := context.WithTimeout(ctx, config.InspectTimeout)
defer cancel() defer cancel()
info, _, err := c.api.ImageInspectWithRaw(inspectCtx, imageID) info, err := c.api.ImageInspect(inspectCtx, imageID)
if err != nil { if err != nil {
return types.ImageInspect{}, fmt.Errorf( return image.InspectResponse{}, fmt.Errorf(
"inspecting image %s: %w", "inspecting image %s: %w",
imageID, imageID,
err, err,

View File

@ -25,7 +25,12 @@ func TestE2E_DockerfileAnalysis(t *testing.T) {
ctx := context.Background() ctx := context.Background()
t.Run("analyze bad-secrets.Dockerfile end-to-end", func(t *testing.T) { t.Run("analyze bad-secrets.Dockerfile end-to-end", func(t *testing.T) {
path := filepath.Join("..", "testdata", "dockerfiles", "bad-secrets.Dockerfile") path := filepath.Join(
"..",
"testdata",
"dockerfiles",
"bad-secrets.Dockerfile",
)
require.FileExists(t, path, "Test file should exist") require.FileExists(t, path, "Test file should exist")
@ -47,7 +52,12 @@ func TestE2E_DockerfileAnalysis(t *testing.T) {
}) })
t.Run("analyze good-security.Dockerfile end-to-end", func(t *testing.T) { t.Run("analyze good-security.Dockerfile end-to-end", func(t *testing.T) {
path := filepath.Join("..", "testdata", "dockerfiles", "good-security.Dockerfile") path := filepath.Join(
"..",
"testdata",
"dockerfiles",
"good-security.Dockerfile",
)
require.FileExists(t, path, "Test file should exist") require.FileExists(t, path, "Test file should exist")
@ -73,7 +83,12 @@ func TestE2E_ComposeAnalysis(t *testing.T) {
ctx := context.Background() ctx := context.Background()
t.Run("analyze bad-docker-socket.yml end-to-end", func(t *testing.T) { t.Run("analyze bad-docker-socket.yml end-to-end", func(t *testing.T) {
path := filepath.Join("..", "testdata", "compose", "bad-docker-socket.yml") path := filepath.Join(
"..",
"testdata",
"compose",
"bad-docker-socket.yml",
)
require.FileExists(t, path, "Test file should exist") require.FileExists(t, path, "Test file should exist")
@ -83,8 +98,11 @@ func TestE2E_ComposeAnalysis(t *testing.T) {
require.NoError(t, err, "Analyze should not return error") require.NoError(t, err, "Analyze should not return error")
require.NotEmpty(t, findings, "Should have findings") require.NotEmpty(t, findings, "Should have findings")
assert.True(t, findings.HasSeverityAtOrAbove(finding.SeverityCritical), assert.True(
"Should detect CRITICAL issues") t,
findings.HasSeverityAtOrAbove(finding.SeverityCritical),
"Should detect CRITICAL issues",
)
counts := findings.CountBySeverity() counts := findings.CountBySeverity()
t.Logf("Findings by severity: %+v", counts) t.Logf("Findings by severity: %+v", counts)
@ -95,7 +113,12 @@ func TestE2E_ComposeAnalysis(t *testing.T) {
}) })
t.Run("analyze good-production.yml end-to-end", func(t *testing.T) { t.Run("analyze good-production.yml end-to-end", func(t *testing.T) {
path := filepath.Join("..", "testdata", "compose", "good-production.yml") path := filepath.Join(
"..",
"testdata",
"compose",
"good-production.yml",
)
require.FileExists(t, path, "Test file should exist") require.FileExists(t, path, "Test file should exist")
@ -104,8 +127,11 @@ func TestE2E_ComposeAnalysis(t *testing.T) {
findings, err := a.Analyze(ctx) findings, err := a.Analyze(ctx)
require.NoError(t, err, "Analyze should not return error") require.NoError(t, err, "Analyze should not return error")
assert.False(t, findings.HasSeverityAtOrAbove(finding.SeverityCritical), assert.False(
"Production compose should have no CRITICAL findings") t,
findings.HasSeverityAtOrAbove(finding.SeverityCritical),
"Production compose should have no CRITICAL findings",
)
counts := findings.CountBySeverity() counts := findings.CountBySeverity()
t.Logf("Findings by severity: %+v", counts) t.Logf("Findings by severity: %+v", counts)
@ -126,22 +152,42 @@ func TestE2E_MultipleFiles(t *testing.T) {
wantIssues bool wantIssues bool
}{ }{
{ {
path: filepath.Join("..", "testdata", "dockerfiles", "bad-secrets.Dockerfile"), path: filepath.Join(
"..",
"testdata",
"dockerfiles",
"bad-secrets.Dockerfile",
),
analyzer: func(p string) analyzer.Analyzer { return analyzer.NewDockerfileAnalyzer(p) }, analyzer: func(p string) analyzer.Analyzer { return analyzer.NewDockerfileAnalyzer(p) },
wantIssues: true, wantIssues: true,
}, },
{ {
path: filepath.Join("..", "testdata", "dockerfiles", "good-minimal.Dockerfile"), path: filepath.Join(
"..",
"testdata",
"dockerfiles",
"good-minimal.Dockerfile",
),
analyzer: func(p string) analyzer.Analyzer { return analyzer.NewDockerfileAnalyzer(p) }, analyzer: func(p string) analyzer.Analyzer { return analyzer.NewDockerfileAnalyzer(p) },
wantIssues: false, wantIssues: false,
}, },
{ {
path: filepath.Join("..", "testdata", "compose", "bad-privileged.yml"), path: filepath.Join(
"..",
"testdata",
"compose",
"bad-privileged.yml",
),
analyzer: func(p string) analyzer.Analyzer { return analyzer.NewComposeAnalyzer(p) }, analyzer: func(p string) analyzer.Analyzer { return analyzer.NewComposeAnalyzer(p) },
wantIssues: true, wantIssues: true,
}, },
{ {
path: filepath.Join("..", "testdata", "compose", "good-production.yml"), path: filepath.Join(
"..",
"testdata",
"compose",
"good-production.yml",
),
analyzer: func(p string) analyzer.Analyzer { return analyzer.NewComposeAnalyzer(p) }, analyzer: func(p string) analyzer.Analyzer { return analyzer.NewComposeAnalyzer(p) },
wantIssues: false, wantIssues: false,
}, },
@ -158,7 +204,12 @@ func TestE2E_MultipleFiles(t *testing.T) {
require.NoError(t, err, "Analyze should not error for %s", f.path) require.NoError(t, err, "Analyze should not error for %s", f.path)
if f.wantIssues { if f.wantIssues {
assert.NotEmpty(t, findings, "File %s should have findings", f.path) assert.NotEmpty(
t,
findings,
"File %s should have findings",
f.path,
)
} }
allFindings = append(allFindings, findings...) allFindings = append(allFindings, findings...)
@ -166,7 +217,11 @@ func TestE2E_MultipleFiles(t *testing.T) {
} }
t.Logf("Total findings across all files: %d", allFindings.Total()) t.Logf("Total findings across all files: %d", allFindings.Total())
assert.NotEmpty(t, allFindings, "Should have findings across all files") assert.NotEmpty(
t,
allFindings,
"Should have findings across all files",
)
counts := allFindings.CountBySeverity() counts := allFindings.CountBySeverity()
t.Logf("Overall severity distribution: %+v", counts) t.Logf("Overall severity distribution: %+v", counts)
@ -191,12 +246,28 @@ func TestE2E_FindingProperties(t *testing.T) {
assert.NotEmpty(t, f.ID, "Finding should have ID") assert.NotEmpty(t, f.ID, "Finding should have ID")
assert.NotEmpty(t, f.RuleID, "Finding should have RuleID") assert.NotEmpty(t, f.RuleID, "Finding should have RuleID")
assert.NotEmpty(t, f.Title, "Finding should have Title") assert.NotEmpty(t, f.Title, "Finding should have Title")
assert.NotEmpty(t, f.Description, "Finding should have Description") assert.NotEmpty(
t,
f.Description,
"Finding should have Description",
)
assert.NotEmpty(t, f.Category, "Finding should have Category") assert.NotEmpty(t, f.Category, "Finding should have Category")
assert.NotEmpty(t, f.Remediation, "Finding should have Remediation") assert.NotEmpty(
t,
f.Remediation,
"Finding should have Remediation",
)
assert.NotZero(t, f.Severity, "Finding should have Severity") assert.NotZero(t, f.Severity, "Finding should have Severity")
assert.NotEmpty(t, f.Target.Type, "Finding should have Target.Type") assert.NotEmpty(
assert.NotEmpty(t, f.Target.Name, "Finding should have Target.Name") t,
f.Target.Type,
"Finding should have Target.Type",
)
assert.NotEmpty(
t,
f.Target.Name,
"Finding should have Target.Name",
)
} }
}) })
@ -205,12 +276,25 @@ func TestE2E_FindingProperties(t *testing.T) {
for _, f := range findings { for _, f := range findings {
if f.Location != nil { if f.Location != nil {
hasLocation = true hasLocation = true
assert.NotEmpty(t, f.Location.Path, "Location should have Path") assert.NotEmpty(
assert.Greater(t, f.Location.Line, 0, "Location should have Line > 0") t,
f.Location.Path,
"Location should have Path",
)
assert.Greater(
t,
f.Location.Line,
0,
"Location should have Line > 0",
)
break break
} }
} }
assert.True(t, hasLocation, "At least one finding should have location info") assert.True(
t,
hasLocation,
"At least one finding should have location info",
)
}) })
t.Run("CIS findings have CIS control info", func(t *testing.T) { t.Run("CIS findings have CIS control info", func(t *testing.T) {
@ -219,8 +303,16 @@ func TestE2E_FindingProperties(t *testing.T) {
if len(f.RuleID) >= 4 && f.RuleID[:4] == "CIS-" { if len(f.RuleID) >= 4 && f.RuleID[:4] == "CIS-" {
hasCIS = true hasCIS = true
if f.CISControl != nil { if f.CISControl != nil {
assert.NotEmpty(t, f.CISControl.ID, "CISControl should have ID") assert.NotEmpty(
assert.NotEmpty(t, f.CISControl.Title, "CISControl should have Title") t,
f.CISControl.ID,
"CISControl should have ID",
)
assert.NotEmpty(
t,
f.CISControl.Title,
"CISControl should have Title",
)
} }
break break
} }
@ -235,7 +327,12 @@ func TestE2E_SeverityFiltering(t *testing.T) {
} }
ctx := context.Background() ctx := context.Background()
path := filepath.Join("..", "testdata", "compose", "bad-docker-socket.yml") path := filepath.Join(
"..",
"testdata",
"compose",
"bad-docker-socket.yml",
)
a := analyzer.NewComposeAnalyzer(path) a := analyzer.NewComposeAnalyzer(path)
findings, err := a.Analyze(ctx) findings, err := a.Analyze(ctx)
@ -247,7 +344,12 @@ func TestE2E_SeverityFiltering(t *testing.T) {
high := findings.BySeverity(finding.SeverityHigh) high := findings.BySeverity(finding.SeverityHigh)
medium := findings.BySeverity(finding.SeverityMedium) medium := findings.BySeverity(finding.SeverityMedium)
t.Logf("CRITICAL: %d, HIGH: %d, MEDIUM: %d", len(critical), len(high), len(medium)) t.Logf(
"CRITICAL: %d, HIGH: %d, MEDIUM: %d",
len(critical),
len(high),
len(medium),
)
assert.NotEmpty(t, critical, "Should have CRITICAL findings") assert.NotEmpty(t, critical, "Should have CRITICAL findings")
@ -278,7 +380,12 @@ func TestE2E_FileNotFound(t *testing.T) {
ctx := context.Background() ctx := context.Background()
t.Run("nonexistent Dockerfile", func(t *testing.T) { t.Run("nonexistent Dockerfile", func(t *testing.T) {
path := filepath.Join("..", "testdata", "dockerfiles", "does-not-exist.Dockerfile") path := filepath.Join(
"..",
"testdata",
"dockerfiles",
"does-not-exist.Dockerfile",
)
a := analyzer.NewDockerfileAnalyzer(path) a := analyzer.NewDockerfileAnalyzer(path)
_, err := a.Analyze(ctx) _, err := a.Analyze(ctx)
@ -288,7 +395,12 @@ func TestE2E_FileNotFound(t *testing.T) {
}) })
t.Run("nonexistent compose file", func(t *testing.T) { t.Run("nonexistent compose file", func(t *testing.T) {
path := filepath.Join("..", "testdata", "compose", "does-not-exist.yml") path := filepath.Join(
"..",
"testdata",
"compose",
"does-not-exist.yml",
)
a := analyzer.NewComposeAnalyzer(path) a := analyzer.NewComposeAnalyzer(path)
_, err := a.Analyze(ctx) _, err := a.Analyze(ctx)

View File

@ -9,8 +9,8 @@ import (
"context" "context"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/CarterPerez-dev/docksec/internal/analyzer"
"github.com/CarterPerez-dev/docksec/internal/analyzer"
"github.com/CarterPerez-dev/docksec/internal/finding" "github.com/CarterPerez-dev/docksec/internal/finding"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"

@ -1 +1 @@
Subproject commit 0eec274fba59ddd373080ae961dd4497eaf2e454 Subproject commit ecbb534e85e8e381e6e89aece4b786db8f7ad172