issue 77 pt3 golangci
This commit is contained in:
parent
1316d31129
commit
7168174006
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]:
|
||||||
|
|
|
||||||
|
|
@ -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(" :.-")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue