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
path: TEMPLATES/fullstack-template/frontend
# Go
- name: simple-vulnerability-scanner
type: go
path: PROJECTS/beginner/simple-vulnerability-scanner
- name: docker-security-audit
type: go
path: PROJECTS/beginner/docker-security-audit
path: PROJECTS/intermediate/docker-security-audit
defaults:
run:
@ -142,9 +145,13 @@ jobs:
if: matrix.type == 'go'
uses: actions/setup-go@v5
with:
go-version: '1.21'
go-version-file: ${{ matrix.path }}/go.mod
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
- name: Run ruff
if: matrix.type == 'ruff'
@ -183,7 +190,7 @@ jobs:
id: golangci
run: |
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 "No golangci-lint errors found!"
else

View File

@ -92,8 +92,24 @@ repos:
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
- repo: local

View File

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

View File

@ -62,6 +62,7 @@ class ScoreWeight:
URL_RATIO_CAP: Final[float] = 0.35
URL_DECODE_CHANGED: Final[float] = 0.15
BASE64_CHARSET: Final[
frozenset[str]
] = frozenset("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=")
@ -75,4 +76,3 @@ BASE32_CHARSET: Final[frozenset[str]] = frozenset("ABCDEFGHIJKLMNOPQRSTUVWXYZ234
HEX_CHARSET: Final[frozenset[str]] = frozenset("0123456789abcdefABCDEF")
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
@dataclass(frozen=True, slots=True)
@dataclass(frozen = True, slots = True)
class DetectionResult:
format: EncodingFormat
confidence: float
@ -152,9 +152,7 @@ def _score_hex(data: str) -> float:
else:
score -= W.HEX_NO_ALPHA_PENALTY
is_consistent_case = (
hex_only == hex_only.lower() or hex_only == hex_only.upper()
)
is_consistent_case = (hex_only == hex_only.lower() or hex_only == hex_only.upper())
if is_consistent_case:
score += W.HEX_CONSISTENT_CASE
@ -188,20 +186,22 @@ def _score_url(data: str) -> float:
decoded = try_decode(data, EncodingFormat.URL)
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:
score += W.URL_DECODE_CHANGED
return min(score, 1.0)
_SCORERS: dict[EncodingFormat, Callable[[str], float]] = {
EncodingFormat.BASE64: _score_base64,
EncodingFormat.BASE64URL: _score_base64url,
EncodingFormat.BASE32: _score_base32,
EncodingFormat.HEX: _score_hex,
EncodingFormat.URL: _score_url,
}
_SCORERS: dict[EncodingFormat,
Callable[[str],
float]] = {
EncodingFormat.BASE64: _score_base64,
EncodingFormat.BASE64URL: _score_base64url,
EncodingFormat.BASE32: _score_base32,
EncodingFormat.HEX: _score_hex,
EncodingFormat.URL: _score_url,
}
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)
results.append(
DetectionResult(
format=fmt,
confidence=round(confidence, 2),
decoded=decoded,
format = fmt,
confidence = round(confidence,
2),
decoded = decoded,
)
)
results.sort(key=lambda r: r.confidence, reverse=True)
results.sort(key = lambda r: r.confidence, reverse = True)
return results

View File

@ -20,7 +20,7 @@ from base64_tool.peeler import PeelResult
from base64_tool.utils import safe_bytes_preview
console = Console(stderr=True)
console = Console(stderr = True)
def is_piped() -> bool:
@ -37,44 +37,49 @@ def print_encoded(result: str, fmt: EncodingFormat) -> None:
write_raw(result)
return
panel = Panel(
Text(result, style="green"),
title=f"[bold cyan]{fmt.value}[/bold cyan] encoded",
border_style="cyan",
Text(result,
style = "green"),
title = f"[bold cyan]{fmt.value}[/bold cyan] encoded",
border_style = "cyan",
)
console.print(panel)
def print_decoded(result: bytes) -> None:
preview = safe_bytes_preview(result, length=4096)
preview = safe_bytes_preview(result, length = 4096)
if is_piped():
write_raw(preview)
return
panel = Panel(
Text(preview, style="green"),
title="[bold cyan]Decoded[/bold cyan]",
border_style="cyan",
Text(preview,
style = "green"),
title = "[bold cyan]Decoded[/bold cyan]",
border_style = "cyan",
)
console.print(panel)
def print_score_breakdown(
scores: dict[EncodingFormat, float],
scores: dict[EncodingFormat,
float],
) -> None:
table = Table(
title="Score Breakdown",
show_header=True,
header_style="bold magenta",
title = "Score Breakdown",
show_header = True,
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(
"Score",
justify="right",
min_width=8,
justify = "right",
min_width = 8,
)
table.add_column("Status", min_width=10)
table.add_column("Status", min_width = 10)
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:
color = _confidence_color(score)
@ -96,7 +101,8 @@ def print_score_breakdown(
def print_detection(
results: list[DetectionResult],
*,
verbose_scores: dict[EncodingFormat, float] | None = None,
verbose_scores: dict[EncodingFormat,
float] | None = None,
) -> None:
if verbose_scores is not None:
print_score_breakdown(verbose_scores)
@ -107,18 +113,18 @@ def print_detection(
return
table = Table(
title="Detection Results",
show_header=True,
header_style="bold magenta",
title = "Detection Results",
show_header = True,
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(
"Confidence",
justify="right",
style="green",
min_width=12,
justify = "right",
style = "green",
min_width = 12,
)
table.add_column("Decoded Preview", style="dim")
table.add_column("Decoded Preview", style = "dim")
for result in results:
confidence_str = f"{result.confidence:.0%}"
@ -171,18 +177,20 @@ def print_peel_result(
console.print()
preview = safe_bytes_preview(result.final_output, length=4096)
preview = safe_bytes_preview(result.final_output, length = 4096)
panel = Panel(
Text(preview, style="bold green"),
title="[bold]Final Output[/bold]",
border_style="green",
subtitle=(f"[dim]{layer_count} layer{suffix} peeled[/dim]"),
Text(preview,
style = "bold green"),
title = "[bold]Final Output[/bold]",
border_style = "green",
subtitle = (f"[dim]{layer_count} layer{suffix} peeled[/dim]"),
)
console.print(panel)
def print_chain_result(
steps: list[tuple[EncodingFormat, str]],
steps: list[tuple[EncodingFormat,
str]],
final: str,
) -> None:
if is_piped():
@ -196,7 +204,7 @@ def print_chain_result(
for i, (fmt, intermediate) in enumerate(steps):
marker = "start" if i == 0 else "step"
arrow = f" [{marker}] " if i == 0 else " -> "
truncated = intermediate[:PREVIEW_LENGTH]
truncated = intermediate[: PREVIEW_LENGTH]
ellipsis = "..." if len(intermediate) > PREVIEW_LENGTH else ""
console.print(
f"{arrow}[cyan]{fmt.value}[/cyan] "
@ -205,10 +213,11 @@ def print_chain_result(
console.print()
panel = Panel(
Text(final, style="green"),
title="[bold]Chain Result[/bold]",
border_style="cyan",
subtitle=f"[dim]{len(steps)} steps[/dim]",
Text(final,
style = "green"),
title = "[bold]Chain Result[/bold]",
border_style = "cyan",
subtitle = f"[dim]{len(steps)} steps[/dim]",
)
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
@dataclass(frozen=True, slots=True)
@dataclass(frozen = True, slots = True)
class PeelLayer:
depth: int
format: EncodingFormat
@ -24,7 +24,7 @@ class PeelLayer:
all_scores: tuple[tuple[EncodingFormat, float], ...] = ()
@dataclass(frozen=True, slots=True)
@dataclass(frozen = True, slots = True)
class PeelResult:
layers: tuple[PeelLayer, ...]
final_output: bytes
@ -52,20 +52,16 @@ def peel(
if detection.decoded is None:
break
scores = (
tuple(score_all_formats(current_text).items())
if verbose
else ()
)
scores = (tuple(score_all_formats(current_text).items()) if verbose else ())
decoded_bytes = detection.decoded
layer = PeelLayer(
depth=depth + 1,
format=detection.format,
confidence=detection.confidence,
encoded_preview=truncate(current_text),
decoded_preview=safe_bytes_preview(decoded_bytes),
all_scores=scores,
depth = depth + 1,
format = detection.format,
confidence = detection.confidence,
encoded_preview = truncate(current_text),
decoded_preview = safe_bytes_preview(decoded_bytes),
all_scores = scores,
)
layers.append(layer)
current_bytes = decoded_bytes
@ -76,7 +72,7 @@ def peel(
break
return PeelResult(
layers=tuple(layers),
final_output=current_bytes,
success=len(layers) > 0,
layers = tuple(layers),
final_output = current_bytes,
success = len(layers) > 0,
)

View File

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

View File

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

View File

@ -134,11 +134,11 @@ func runInit() error {
name := filepath.Base(dir)
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",
[]byte(content),
0o644,
); err != nil { //nolint:gosec
); err != nil {
return fmt.Errorf("write pyproject.toml: %w", err)
}

View File

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

View File

@ -1,5 +1,5 @@
/*
AngelaMos | 2026
© AngelaMos | 2026
container_test.go
*/
@ -12,23 +12,33 @@ import (
"testing"
"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/require"
)
func loadContainerJSON(t *testing.T, filename string) types.ContainerJSON {
func loadContainerJSON(
t *testing.T,
filename string,
) container.InspectResponse {
t.Helper()
path := filepath.Join("..", "..", "tests", "testdata", "containers", filename)
path := filepath.Join(
"..",
"..",
"tests",
"testdata",
"containers",
filename,
)
data, err := os.ReadFile(path)
require.NoError(t, err, "Failed to read container JSON file")
var container types.ContainerJSON
err = json.Unmarshal(data, &container)
var ctr container.InspectResponse
err = json.Unmarshal(data, &ctr)
require.NoError(t, err, "Failed to unmarshal container JSON")
return container
return ctr
}
func TestContainerAnalyzer_PrivilegedContainer(t *testing.T) {

View File

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

View File

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

View File

@ -25,7 +25,12 @@ func TestE2E_DockerfileAnalysis(t *testing.T) {
ctx := context.Background()
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")
@ -47,7 +52,12 @@ func TestE2E_DockerfileAnalysis(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")
@ -73,7 +83,12 @@ func TestE2E_ComposeAnalysis(t *testing.T) {
ctx := context.Background()
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")
@ -83,8 +98,11 @@ func TestE2E_ComposeAnalysis(t *testing.T) {
require.NoError(t, err, "Analyze should not return error")
require.NotEmpty(t, findings, "Should have findings")
assert.True(t, findings.HasSeverityAtOrAbove(finding.SeverityCritical),
"Should detect CRITICAL issues")
assert.True(
t,
findings.HasSeverityAtOrAbove(finding.SeverityCritical),
"Should detect CRITICAL issues",
)
counts := findings.CountBySeverity()
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) {
path := filepath.Join("..", "testdata", "compose", "good-production.yml")
path := filepath.Join(
"..",
"testdata",
"compose",
"good-production.yml",
)
require.FileExists(t, path, "Test file should exist")
@ -104,8 +127,11 @@ func TestE2E_ComposeAnalysis(t *testing.T) {
findings, err := a.Analyze(ctx)
require.NoError(t, err, "Analyze should not return error")
assert.False(t, findings.HasSeverityAtOrAbove(finding.SeverityCritical),
"Production compose should have no CRITICAL findings")
assert.False(
t,
findings.HasSeverityAtOrAbove(finding.SeverityCritical),
"Production compose should have no CRITICAL findings",
)
counts := findings.CountBySeverity()
t.Logf("Findings by severity: %+v", counts)
@ -126,22 +152,42 @@ func TestE2E_MultipleFiles(t *testing.T) {
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) },
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) },
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) },
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) },
wantIssues: false,
},
@ -158,7 +204,12 @@ func TestE2E_MultipleFiles(t *testing.T) {
require.NoError(t, err, "Analyze should not error for %s", f.path)
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...)
@ -166,7 +217,11 @@ func TestE2E_MultipleFiles(t *testing.T) {
}
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()
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.RuleID, "Finding should have RuleID")
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.Remediation, "Finding should have Remediation")
assert.NotEmpty(
t,
f.Remediation,
"Finding should have Remediation",
)
assert.NotZero(t, f.Severity, "Finding should have Severity")
assert.NotEmpty(t, f.Target.Type, "Finding should have Target.Type")
assert.NotEmpty(t, f.Target.Name, "Finding should have Target.Name")
assert.NotEmpty(
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 {
if f.Location != nil {
hasLocation = true
assert.NotEmpty(t, f.Location.Path, "Location should have Path")
assert.Greater(t, f.Location.Line, 0, "Location should have Line > 0")
assert.NotEmpty(
t,
f.Location.Path,
"Location should have Path",
)
assert.Greater(
t,
f.Location.Line,
0,
"Location should have Line > 0",
)
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) {
@ -219,8 +303,16 @@ func TestE2E_FindingProperties(t *testing.T) {
if len(f.RuleID) >= 4 && f.RuleID[:4] == "CIS-" {
hasCIS = true
if f.CISControl != nil {
assert.NotEmpty(t, f.CISControl.ID, "CISControl should have ID")
assert.NotEmpty(t, f.CISControl.Title, "CISControl should have Title")
assert.NotEmpty(
t,
f.CISControl.ID,
"CISControl should have ID",
)
assert.NotEmpty(
t,
f.CISControl.Title,
"CISControl should have Title",
)
}
break
}
@ -235,7 +327,12 @@ func TestE2E_SeverityFiltering(t *testing.T) {
}
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)
findings, err := a.Analyze(ctx)
@ -247,7 +344,12 @@ func TestE2E_SeverityFiltering(t *testing.T) {
high := findings.BySeverity(finding.SeverityHigh)
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")
@ -278,7 +380,12 @@ func TestE2E_FileNotFound(t *testing.T) {
ctx := context.Background()
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)
_, err := a.Analyze(ctx)
@ -288,7 +395,12 @@ func TestE2E_FileNotFound(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)
_, err := a.Analyze(ctx)

View File

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

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