diff --git a/.github/config/htmlhint-sigilix.json b/.github/config/htmlhint-sigilix.json new file mode 100644 index 0000000..e089ac0 --- /dev/null +++ b/.github/config/htmlhint-sigilix.json @@ -0,0 +1,10 @@ +{ + "doctype-first": true, + "tag-pair": true, + "attr-no-duplication": true, + "id-unique": true, + "src-not-empty": true, + "alt-require": true, + "inline-style-disabled": false, + "inline-script-disabled": false +} diff --git a/.github/config/stylelint-sigilix.json b/.github/config/stylelint-sigilix.json new file mode 100644 index 0000000..bdad4cd --- /dev/null +++ b/.github/config/stylelint-sigilix.json @@ -0,0 +1,8 @@ +{ + "rules": { + "block-no-empty": true, + "declaration-block-no-duplicate-properties": true, + "property-no-unknown": true, + "selector-type-no-unknown": true + } +} diff --git a/.github/config/tool-manifest.json b/.github/config/tool-manifest.json index 854b38a..c4f1893 100644 --- a/.github/config/tool-manifest.json +++ b/.github/config/tool-manifest.json @@ -25,11 +25,21 @@ "env": "PYLINT_ENABLED", "output": "pylint.sarif" }, + { + "id": "flake8", + "env": "FLAKE8_ENABLED", + "output": "flake8.sarif" + }, { "id": "knip", "env": "KNIP_ENABLED", "output": "knip.sarif" }, + { + "id": "golangci-lint", + "env": "GOLANGCI_LINT_ENABLED", + "output": "golangci-lint.sarif" + }, { "id": "actionlint", "env": "ACTIONLINT_ENABLED", @@ -95,6 +105,16 @@ "env": "AST_GREP_ENABLED", "output": "ast-grep.sarif" }, + { + "id": "htmlhint", + "env": "HTMLHINT_ENABLED", + "output": "htmlhint.sarif" + }, + { + "id": "stylelint", + "env": "STYLELINT_ENABLED", + "output": "stylelint.sarif" + }, { "id": "yamllint", "env": "YAMLLINT_ENABLED", diff --git a/.github/scripts/flake8_to_sarif.py b/.github/scripts/flake8_to_sarif.py new file mode 100644 index 0000000..63edb78 --- /dev/null +++ b/.github/scripts/flake8_to_sarif.py @@ -0,0 +1,65 @@ +import argparse +import re +import sys + +from sarif_converter_common import make_document, make_result, write_json_file + + +FLAKE8_TOOL_ID = "flake8" +FLAKE8_TOOL_NAME = "Flake8" +FLAKE8_INFORMATION_URI = "https://flake8.pycqa.org/" + +_LINE_RE = re.compile(r"^(?P.+):(?P\d+):(?P\d+): (?P[A-Z]\d{3}) (?P.*)$") + + +def convert_flake8_output(text, base_dir=".", cap=None): + results = [] + for line in str(text or "").splitlines(): + match = _LINE_RE.match(line) + if not match: + continue + results.append( + make_result( + match.group("code"), + _level_for_code(match.group("code")), + match.group("message"), + match.group("path"), + line=_int_or_none(match.group("line")), + column=_int_or_none(match.group("column")), + base_dir=base_dir, + ) + ) + return make_document(FLAKE8_TOOL_NAME, FLAKE8_TOOL_ID, results, information_uri=FLAKE8_INFORMATION_URI, cap=cap) + + +def _int_or_none(value): + try: + return int(value) + except (TypeError, ValueError): + return None + + +def _level_for_code(code): + return "error" if str(code or "").upper().startswith("E9") else "warning" + + +def _main(argv): + parser = argparse.ArgumentParser(description="Convert Flake8 text output to Sigilix SARIF.") + parser.add_argument("input") + parser.add_argument("output") + parser.add_argument("--base-dir", default=".") + parser.add_argument("--cap", type=int) + args = parser.parse_args(argv) + + try: + with open(args.input, encoding="utf-8") as handle: + content = handle.read() + except OSError: + content = "" + document = convert_flake8_output(content, base_dir=args.base_dir, cap=args.cap) + write_json_file(args.output, document) + return 0 + + +if __name__ == "__main__": + sys.exit(_main(sys.argv[1:])) diff --git a/.github/scripts/language_config_tools_workflow_test.py b/.github/scripts/language_config_tools_workflow_test.py new file mode 100644 index 0000000..b09b73d --- /dev/null +++ b/.github/scripts/language_config_tools_workflow_test.py @@ -0,0 +1,271 @@ +import json +import os +import re +import unittest + + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +WORKFLOW_PATH = os.path.join(ROOT, ".github", "workflows", "scan.yml") +MANIFEST_PATH = os.path.join(ROOT, ".github", "config", "tool-manifest.json") +CONFIG_DIR = os.path.join(ROOT, ".github", "config") +SCRIPT_DIR = os.path.join(ROOT, ".github", "scripts") + +NEW_TOOL_OUTPUTS = { + "flake8": "flake8.sarif", + "golangci-lint": "golangci-lint.sarif", + "htmlhint": "htmlhint.sarif", + "stylelint": "stylelint.sarif", +} + + +class LanguageConfigToolsWorkflowTest(unittest.TestCase): + def read_file(self, path): + with open(path, encoding="utf-8") as handle: + return handle.read() + + def workflow_text(self): + return self.read_file(WORKFLOW_PATH) + + def workflow_input_block(self, input_name): + pattern = rf"\n {re.escape(input_name)}:\n(?P(?: .+\n)+)" + match = re.search(pattern, self.workflow_text()) + self.assertIsNotNone(match) + return match.group("block") + + def workflow_step_block(self, step_name): + pattern = rf"(?ms)^ - name: {re.escape(step_name)}\n.+?(?=^ - name: |\Z)" + match = re.search(pattern, self.workflow_text()) + self.assertIsNotNone(match) + return match.group(0) + + def manifest_rows(self): + with open(MANIFEST_PATH, encoding="utf-8") as handle: + return {row["id"]: row for row in json.load(handle)["tools"]} + + def config_json(self, filename): + with open(os.path.join(CONFIG_DIR, filename), encoding="utf-8") as handle: + return json.load(handle) + + def script_text(self, filename): + return self.read_file(os.path.join(SCRIPT_DIR, filename)) + + def test_new_tools_are_default_on_and_manifested(self): + text = self.workflow_text() + rows = self.manifest_rows() + + for tool_id, output in NEW_TOOL_OUTPUTS.items(): + env_var = tool_id.upper().replace("-", "_") + "_ENABLED" + self.assertIn(" default: true\n", self.workflow_input_block(tool_id)) + self.assertIn(f"{env_var}: ${{{{ inputs.{tool_id} }}}}", text) + self.assertEqual(rows[tool_id], {"id": tool_id, "env": env_var, "output": output}) + + self.assertIn("GOLANGCI_LINT_VERSION: \"2.12.2\"", text) + self.assertIn("GOLANGCI_LINT_LINUX_AMD64_SHA256:", text) + self.assertIn("FLAKE8_VERSION: \"7.3.0\"", text) + self.assertIn("HTMLHINT_NPM_INTEGRITY:", text) + self.assertIn("HTMLHINT_VERSION: \"1.9.2\"", text) + self.assertIn("STYLELINT_NPM_INTEGRITY:", text) + self.assertIn("STYLELINT_VERSION: \"17.12.0\"", text) + self.assertIn("TFLINT_LINUX_AMD64_SHA256:", text) + + def test_workflow_delegates_new_tools_to_runner_scripts(self): + expectations = { + "Run Flake8 to SARIF": "run_flake8.sh", + "Run golangci-lint to SARIF": "run_golangci_lint.sh", + "Run HTMLHint to SARIF": "run_htmlhint.sh", + "Run Stylelint to SARIF": "run_stylelint.sh", + "Run TFLint to SARIF": "run_tflint.sh", + } + + for step_name, script_name in expectations.items(): + block = self.workflow_step_block(step_name) + self.assertIn(f'bash "$RUNNER_DIR/.github/scripts/{script_name}"', block) + + def test_flake8_wrapper_uses_marker_gated_high_confidence_profile(self): + text = self.script_text("run_flake8.sh") + + self.assertIn("FLAKE8_VERSION", text) + self.assertIn("python3 -m venv \"$flake8_venv\"", text) + self.assertIn('"flake8==${FLAKE8_VERSION}"', text) + self.assertIn("find_flake8_marker", text) + self.assertIn("No .flake8 marker found", text) + self.assertIn("--isolated", text) + self.assertIn("--select=E9,F63,F7,F82", text) + self.assertIn("--format=%(path)s:%(row)d:%(col)d: %(code)s %(text)s", text) + self.assertIn("flake8_to_sarif.py", text) + + def test_golangci_wrapper_avoids_caller_config_and_requires_go_files(self): + text = self.script_text("run_golangci_lint.sh") + + self.assertIn("GOLANGCI_LINT_VERSION", text) + self.assertIn("golangci-lint-${GOLANGCI_LINT_VERSION}-linux-amd64.tar.gz", text) + self.assertIn("discover_go_files", text) + self.assertIn("No Go files found", text) + self.assertIn("No root go.mod found", text) + self.assertIn("GOLANGCI_LINT_LINUX_AMD64_SHA256", text) + self.assertIn("sha256sum -c --strict", text) + self.assertIn("--no-config", text) + self.assertIn("--default=standard", text) + self.assertIn("--output.sarif.path=\"$raw\"", text) + self.assertIn("--issues-exit-code=0", text) + self.assertIn("sigilix_sarif_contract.py", text) + + def test_htmlhint_wrapper_uses_runner_config_and_native_sarif(self): + text = self.script_text("run_htmlhint.sh") + config = self.config_json("htmlhint-sigilix.json") + + self.assertIn("HTMLHINT_VERSION", text) + self.assertIn("HTMLHINT_NPM_INTEGRITY", text) + self.assertIn("npm pack --json --silent", text) + self.assertIn("PACK_JSON", text) + self.assertIn("verify_package_integrity", text) + self.assertIn("htmlhint-${HTMLHINT_VERSION}.XXXXXX", text) + self.assertIn("htmlhint-[0-9]+[.][0-9]+[.][0-9]+[.]tgz", text) + self.assertIn("--ignore-scripts --omit=optional", text) + self.assertIn("htmlhint-sigilix.json", text) + self.assertIn("--format sarif", text) + self.assertNotIn("--warn", text) + self.assertIn("sigilix_sarif_contract.py", text) + self.assertTrue(config["doctype-first"]) + self.assertTrue(config["tag-pair"]) + self.assertTrue(config["attr-no-duplication"]) + self.assertTrue(config["id-unique"]) + self.assertTrue(config["src-not-empty"]) + self.assertTrue(config["alt-require"]) + self.assertFalse(config["inline-style-disabled"]) + self.assertFalse(config["inline-script-disabled"]) + + def test_stylelint_wrapper_uses_runner_config_and_json_converter(self): + text = self.script_text("run_stylelint.sh") + config = self.config_json("stylelint-sigilix.json") + + self.assertIn("STYLELINT_VERSION", text) + self.assertIn("STYLELINT_NPM_INTEGRITY", text) + self.assertIn("npm pack --json --silent", text) + self.assertIn("PACK_JSON", text) + self.assertIn("verify_package_integrity", text) + self.assertIn("stylelint-${STYLELINT_VERSION}.XXXXXX", text) + self.assertIn("stylelint-[0-9]+[.][0-9]+[.][0-9]+[.]tgz", text) + self.assertIn("--ignore-scripts --omit=optional", text) + self.assertIn("stylelint-sigilix.json", text) + self.assertIn("--formatter json", text) + self.assertIn("--output-file \"$json\"", text) + self.assertIn("--allow-empty-input", text) + self.assertIn("stylelint_to_sarif.py", text) + self.assertEqual(config["rules"]["block-no-empty"], True) + self.assertEqual(config["rules"]["declaration-block-no-duplicate-properties"], True) + self.assertEqual(config["rules"]["property-no-unknown"], True) + self.assertEqual(config["rules"]["selector-type-no-unknown"], True) + + def test_tflint_is_default_on_and_scripted_with_terraform_preflight(self): + text = self.workflow_text() + script = self.script_text("run_tflint.sh") + + self.assertIn(" default: true\n", self.workflow_input_block("tflint")) + self.assertIn("TFLINT_ENABLED: ${{ inputs.tflint }}", text) + self.assertIn("TFLINT_LINUX_AMD64_SHA256", script) + self.assertIn('rm -f "$files_list" "$RUNNER_TEMP/tflint.zip" "$RUNNER_TEMP/tflint"', script) + self.assertIn("sha256sum -c --strict", script) + self.assertIn("TFLint binary missing or not executable", script) + self.assertIn("TFLint installed version mismatch", script) + self.assertIn("discover_terraform_files", script) + self.assertIn("No Terraform files found", script) + self.assertIn("tflint_linux_amd64.zip", script) + self.assertIn("--recursive --format sarif", script) + self.assertIn("sigilix_sarif_contract.py", script) + + +class LanguageConfigToolsConverterTest(unittest.TestCase): + def assert_sigilix_properties(self, document, tool_id): + self.assertEqual(document["version"], "2.1.0") + self.assertEqual(len(document["runs"]), 1) + properties = document["runs"][0]["tool"]["driver"]["properties"] + self.assertEqual(properties["sigilixToolId"], tool_id) + self.assertEqual(properties["sigilixSource"], "deterministic-tool") + + def test_flake8_output_converts_to_sarif(self): + from flake8_to_sarif import convert_flake8_output + + document = convert_flake8_output( + "/repo/app.py:2:9: F821 undefined name 'missing'\n" + "/repo/broken.py:1:1: E999 SyntaxError: invalid syntax\n" + "not a flake8 line\n", + base_dir="/repo", + ) + + self.assert_sigilix_properties(document, "flake8") + results = document["runs"][0]["results"] + self.assertEqual([result["ruleId"] for result in results], ["F821", "E999"]) + self.assertEqual([result["level"] for result in results], ["warning", "error"]) + self.assertEqual(results[0]["message"]["text"], "undefined name 'missing'") + self.assertEqual(results[0]["locations"][0]["physicalLocation"]["artifactLocation"]["uri"], "app.py") + self.assertEqual(results[0]["locations"][0]["physicalLocation"]["region"], {"startLine": 2, "startColumn": 9}) + + def test_flake8_level_mapping_is_limited_to_e9_parse_errors(self): + from flake8_to_sarif import _level_for_code + + self.assertEqual(_level_for_code("E999"), "error") + self.assertEqual(_level_for_code("e901"), "error") + self.assertEqual(_level_for_code("E101"), "warning") + self.assertEqual(_level_for_code("F401"), "warning") + self.assertEqual(_level_for_code("W503"), "warning") + + def test_stylelint_json_converts_to_sarif(self): + from stylelint_to_sarif import convert_stylelint_json + + document = convert_stylelint_json( + [ + { + "source": "/repo/src/app.css", + "warnings": [ + { + "line": 3, + "column": 5, + "endLine": 3, + "endColumn": 10, + "rule": "declaration-block-no-duplicate-properties", + "severity": "error", + "text": "Duplicate property \"color\" (declaration-block-no-duplicate-properties)", + } + ], + } + ], + base_dir="/repo", + ) + + self.assert_sigilix_properties(document, "stylelint") + result = document["runs"][0]["results"][0] + self.assertEqual(result["ruleId"], "declaration-block-no-duplicate-properties") + self.assertEqual(result["level"], "error") + self.assertIn("Duplicate property", result["message"]["text"]) + self.assertEqual(result["locations"][0]["physicalLocation"]["artifactLocation"]["uri"], "src/app.css") + self.assertEqual(result["locations"][0]["physicalLocation"]["region"]["startLine"], 3) + + def test_stylelint_converter_drops_end_region_without_start_region(self): + from stylelint_to_sarif import convert_stylelint_json + + document = convert_stylelint_json( + [ + { + "source": "/repo/src/app.css", + "warnings": [ + { + "endLine": 7, + "endColumn": 4, + "rule": "stylelint", + "severity": "info", + "text": "Malformed upstream region", + } + ], + } + ], + base_dir="/repo", + ) + + result = document["runs"][0]["results"][0] + self.assertEqual(result["level"], "note") + self.assertNotIn("region", result["locations"][0]["physicalLocation"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/run_flake8.sh b/.github/scripts/run_flake8.sh new file mode 100644 index 0000000..31550a4 --- /dev/null +++ b/.github/scripts/run_flake8.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${FLAKE8_VERSION:?}" +: "${RESULT_CAP:?}" +: "${RUNNER_DIR:?}" +: "${RUNNER_TEMP:?}" +: "${SARIF_DIR:?}" +: "${SOURCE_DIR:?}" + +SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd -P)" +RUNNER_DIR="$(cd "$RUNNER_DIR" && pwd -P)" + +raw="$SARIF_DIR/flake8.txt" +out="$SARIF_DIR/flake8.sarif" +flake8_venv="$RUNNER_TEMP/flake8-${FLAKE8_VERSION}" +flake8_python="$flake8_venv/bin/python" +files_list="" + +mkdir -p "$SARIF_DIR" "$RUNNER_TEMP" +files_list="$(mktemp "$RUNNER_TEMP/flake8-files.XXXXXX")" + +cleanup_flake8() { + rm -rf "$flake8_venv" + if [ -n "$files_list" ]; then rm -f "$files_list"; fi +} +trap cleanup_flake8 EXIT + +emit_empty_raw() { + : > "$raw" +} + +discover_python_files() { + find -P . \ + \( -type d \( -name '.git' -o -name 'node_modules' -o -name 'dist' -o -name 'build' \ + -o -name 'coverage' -o -name '.next' -o -name 'out' -o -name '.venv' \ + -o -name 'vendor' -o -name '__pycache__' -o -name '.tox' -o -name '.mypy_cache' \ + -o -name '.pytest_cache' -o -name '.terraform' \) -prune \) -o \ + \( -type f -name '*.py' -print0 \) +} + +find_flake8_marker() { + if [ -f .flake8 ]; then + printf '%s\n' .flake8 + fi +} + +cd "$SOURCE_DIR" +flake8_marker="$(find_flake8_marker)" +if [ -z "$flake8_marker" ]; then + echo "::notice::No .flake8 marker found - emitting empty Flake8 SARIF run." + emit_empty_raw +elif ! discover_python_files > "$files_list"; then + echo "::warning::Flake8 file discovery failed - emitting empty Flake8 SARIF run." + emit_empty_raw +else + files=() + while IFS= read -r -d '' file; do + files+=("$file") + done < "$files_list" + if [ "${#files[@]}" -eq 0 ]; then + emit_empty_raw + elif [[ ! "$FLAKE8_VERSION" =~ ^[0-9]+[.][0-9]+[.][0-9]+$ ]]; then + echo "::warning::Flake8 version must be a pinned x.y.z version - emitting empty Flake8 SARIF run." + emit_empty_raw + elif ! python3 -m venv "$flake8_venv"; then + echo "::warning::Flake8 venv creation failed - emitting empty Flake8 SARIF run." + emit_empty_raw + elif ! "$flake8_python" -m pip install --quiet --disable-pip-version-check "flake8==${FLAKE8_VERSION}"; then + echo "::warning::Flake8 install failed - emitting empty Flake8 SARIF run." + emit_empty_raw + elif ! flake8_version="$("$flake8_python" -m flake8 --version 2>/dev/null | awk '{print $1}' | head -n 1)"; then + echo "::warning::Flake8 version check failed - emitting empty Flake8 SARIF run." + emit_empty_raw + elif [ "$flake8_version" != "$FLAKE8_VERSION" ]; then + echo "::warning::Flake8 installed version mismatch - emitting empty Flake8 SARIF run." + emit_empty_raw + else + "$flake8_python" -m flake8 \ + --isolated \ + --select=E9,F63,F7,F82 \ + "--format=%(path)s:%(row)d:%(col)d: %(code)s %(text)s" \ + -- \ + "${files[@]}" > "$raw" || true + fi +fi + +python3 "$RUNNER_DIR/.github/scripts/flake8_to_sarif.py" "$raw" "$out" \ + --base-dir "$SOURCE_DIR" --cap "$RESULT_CAP" diff --git a/.github/scripts/run_golangci_lint.sh b/.github/scripts/run_golangci_lint.sh new file mode 100644 index 0000000..c267797 --- /dev/null +++ b/.github/scripts/run_golangci_lint.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${GOLANGCI_LINT_VERSION:?}" +: "${GOLANGCI_LINT_LINUX_AMD64_SHA256:?}" +: "${RESULT_CAP:?}" +: "${RUNNER_DIR:?}" +: "${RUNNER_TEMP:?}" +: "${SARIF_DIR:?}" +: "${SOURCE_DIR:?}" + +SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd -P)" +RUNNER_DIR="$(cd "$RUNNER_DIR" && pwd -P)" + +raw="$SARIF_DIR/golangci-lint.raw.sarif" +out="$SARIF_DIR/golangci-lint.sarif" +archive="$RUNNER_TEMP/golangci-lint-${GOLANGCI_LINT_VERSION}-linux-amd64.tar.gz" +golangci_dir="$RUNNER_TEMP/golangci-lint-${GOLANGCI_LINT_VERSION}-linux-amd64" +golangci_bin="$golangci_dir/golangci-lint" +files_list="" + +mkdir -p "$SARIF_DIR" "$RUNNER_TEMP" +files_list="$(mktemp "$RUNNER_TEMP/golangci-lint-files.XXXXXX")" + +cleanup_golangci_lint() { + rm -f "$files_list" "$archive" + rm -rf "$golangci_dir" +} +trap cleanup_golangci_lint EXIT + +emit_empty_sarif() { + printf '{"version":"2.1.0","runs":[]}' > "$raw" +} + +discover_go_files() { + find -P . \ + \( -type d \( -name '.git' -o -name 'node_modules' -o -name 'dist' -o -name 'build' \ + -o -name 'coverage' -o -name '.next' -o -name 'out' -o -name 'vendor' \ + -o -name '.terraform' \) -prune \) -o \ + \( -type f \( -name '*.go' -o -name 'go.mod' \) -print0 \) +} + +cd "$SOURCE_DIR" +if ! discover_go_files > "$files_list"; then + echo "::warning::golangci-lint file discovery failed - emitting empty golangci-lint SARIF run." + emit_empty_sarif +else + if ! grep -qz . "$files_list"; then + echo "::notice::No Go files found - emitting empty golangci-lint SARIF run." + emit_empty_sarif + elif [ ! -f go.mod ]; then + echo "::notice::No root go.mod found - emitting empty golangci-lint SARIF run." + emit_empty_sarif + elif [[ ! "$GOLANGCI_LINT_VERSION" =~ ^[0-9]+[.][0-9]+[.][0-9]+$ ]]; then + echo "::warning::golangci-lint version must be a pinned x.y.z version - emitting empty golangci-lint SARIF run." + emit_empty_sarif + elif [[ ! "$GOLANGCI_LINT_LINUX_AMD64_SHA256" =~ ^[0-9a-f]{64}$ ]]; then + echo "::warning::golangci-lint checksum must be a pinned SHA256 value - emitting empty golangci-lint SARIF run." + emit_empty_sarif + elif ! curl -fsSL -o "$archive" \ + "https://github.com/golangci/golangci-lint/releases/download/v${GOLANGCI_LINT_VERSION}/golangci-lint-${GOLANGCI_LINT_VERSION}-linux-amd64.tar.gz"; then + echo "::warning::golangci-lint download failed - emitting empty golangci-lint SARIF run." + emit_empty_sarif + elif ! printf '%s %s\n' "$GOLANGCI_LINT_LINUX_AMD64_SHA256" "$archive" | sha256sum -c --strict -; then + echo "::warning::golangci-lint checksum mismatch - emitting empty golangci-lint SARIF run." + emit_empty_sarif + elif ! tar -xzf "$archive" -C "$RUNNER_TEMP"; then + echo "::warning::golangci-lint extract failed - emitting empty golangci-lint SARIF run." + emit_empty_sarif + elif [ ! -x "$golangci_bin" ]; then + echo "::warning::golangci-lint binary missing after extract - emitting empty golangci-lint SARIF run." + emit_empty_sarif + elif ! golangci_version="$("$golangci_bin" --version 2>/dev/null)"; then + echo "::warning::golangci-lint version check failed - emitting empty golangci-lint SARIF run." + emit_empty_sarif + elif ! printf '%s\n' "$golangci_version" | grep -q "version ${GOLANGCI_LINT_VERSION}\\b"; then + echo "::warning::golangci-lint installed version mismatch - emitting empty golangci-lint SARIF run." + emit_empty_sarif + else + "$golangci_bin" run \ + --no-config \ + --default=standard \ + --timeout=5m \ + --issues-exit-code=0 \ + --output.sarif.path="$raw" \ + ./... || true + if [ ! -s "$raw" ]; then + echo "::warning::golangci-lint scan produced no SARIF output - emitting empty golangci-lint SARIF run." + emit_empty_sarif + fi + fi +fi + +python3 "$RUNNER_DIR/.github/scripts/sigilix_sarif_contract.py" \ + golangci-lint "$raw" "$out" --cap "$RESULT_CAP" --ensure-run diff --git a/.github/scripts/run_htmlhint.sh b/.github/scripts/run_htmlhint.sh new file mode 100644 index 0000000..c171b7b --- /dev/null +++ b/.github/scripts/run_htmlhint.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${HTMLHINT_VERSION:?}" +: "${HTMLHINT_NPM_INTEGRITY:?}" +: "${RESULT_CAP:?}" +: "${RUNNER_DIR:?}" +: "${RUNNER_TEMP:?}" +: "${SARIF_DIR:?}" +: "${SOURCE_DIR:?}" + +SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd -P)" +RUNNER_DIR="$(cd "$RUNNER_DIR" && pwd -P)" + +raw="$SARIF_DIR/htmlhint.raw.sarif" +out="$SARIF_DIR/htmlhint.sarif" +htmlhint_config="$RUNNER_DIR/.github/config/htmlhint-sigilix.json" +htmlhint_install_dir="" +htmlhint_bin="" +htmlhint_package="" +files_list="" + +mkdir -p "$SARIF_DIR" "$RUNNER_TEMP" +files_list="$(mktemp "$RUNNER_TEMP/htmlhint-files.XXXXXX")" + +cleanup_htmlhint() { + rm -f "$files_list" + if [ -n "$htmlhint_install_dir" ]; then rm -rf "$htmlhint_install_dir"; fi +} +trap cleanup_htmlhint EXIT + +emit_empty_sarif() { + printf '{"version":"2.1.0","runs":[]}' > "$raw" +} + +verify_package_integrity() { + local actual + actual="sha512-$(openssl dgst -sha512 -binary "$1" | openssl base64 -A)" + [ "$actual" = "$HTMLHINT_NPM_INTEGRITY" ] +} + +discover_html_files() { + find -P . \ + \( -type d \( -name '.git' -o -name 'node_modules' -o -name 'dist' -o -name 'build' \ + -o -name 'coverage' -o -name '.next' -o -name 'out' -o -name 'vendor' \) -prune \) -o \ + \( -type f \( -name '*.html' -o -name '*.htm' -o -name '*.xhtml' \) -print0 \) +} + +cd "$SOURCE_DIR" +if [ ! -f "$htmlhint_config" ]; then + echo "::warning::HTMLHint Sigilix config missing at $htmlhint_config - emitting empty HTMLHint SARIF run." + emit_empty_sarif +elif ! discover_html_files > "$files_list"; then + echo "::warning::HTMLHint file discovery failed - emitting empty HTMLHint SARIF run." + emit_empty_sarif +else + files=() + while IFS= read -r -d '' file; do + files+=("$file") + done < "$files_list" + if [ "${#files[@]}" -eq 0 ]; then + emit_empty_sarif + elif [[ ! "$HTMLHINT_VERSION" =~ ^[0-9]+[.][0-9]+[.][0-9]+$ ]]; then + echo "::warning::HTMLHint version must be a pinned x.y.z version - emitting empty HTMLHint SARIF run." + emit_empty_sarif + elif [[ ! "$HTMLHINT_NPM_INTEGRITY" =~ ^sha512-[A-Za-z0-9+/]+={0,2}$ ]]; then + echo "::warning::HTMLHint package integrity must be a pinned sha512 value - emitting empty HTMLHint SARIF run." + emit_empty_sarif + else + htmlhint_can_scan=true + htmlhint_install_dir="$(mktemp -d "$RUNNER_TEMP/htmlhint-${HTMLHINT_VERSION}.XXXXXX")" + htmlhint_bin="$htmlhint_install_dir/node_modules/.bin/htmlhint" + if ! htmlhint_package_json="$(npm pack --json --silent --pack-destination "$htmlhint_install_dir" \ + --registry=https://registry.npmjs.org \ + "htmlhint@${HTMLHINT_VERSION}")"; then + echo "::warning::HTMLHint package download failed - emitting empty HTMLHint SARIF run." + htmlhint_can_scan=false + elif ! htmlhint_package="$(PACK_JSON="$htmlhint_package_json" python3 - <<'PY' +import json +import os +import sys + +try: + data = json.loads(os.environ["PACK_JSON"]) + if not isinstance(data, list) or len(data) != 1: + raise ValueError("expected one packed package") + filename = data[0].get("filename") + if not isinstance(filename, str) or not filename: + raise ValueError("missing packed filename") + print(filename) +except Exception: + sys.exit(1) +PY +)"; then + echo "::warning::HTMLHint package download returned invalid metadata - emitting empty HTMLHint SARIF run." + htmlhint_can_scan=false + else + package_name="${htmlhint_package##*/}" + if [[ ! "$package_name" =~ ^htmlhint-[0-9]+[.][0-9]+[.][0-9]+[.]tgz$ ]]; then + echo "::warning::HTMLHint package download returned an unexpected filename - emitting empty HTMLHint SARIF run." + htmlhint_can_scan=false + else + htmlhint_package="$htmlhint_install_dir/$package_name" + fi + fi + + if [ "$htmlhint_can_scan" = true ] && [ ! -f "$htmlhint_package" ]; then + echo "::warning::HTMLHint package tarball missing after download - emitting empty HTMLHint SARIF run." + htmlhint_can_scan=false + elif [ "$htmlhint_can_scan" = true ] && ! verify_package_integrity "$htmlhint_package"; then + echo "::warning::HTMLHint package integrity mismatch - emitting empty HTMLHint SARIF run." + rm -f "$htmlhint_package" + htmlhint_can_scan=false + elif [ "$htmlhint_can_scan" = true ] && ! npm install --silent --prefix "$htmlhint_install_dir" --ignore-scripts --omit=optional \ + --registry=https://registry.npmjs.org --no-audit --no-fund \ + "$htmlhint_package" >/dev/null; then + echo "::warning::HTMLHint package install failed - emitting empty HTMLHint SARIF run." + htmlhint_can_scan=false + elif [ "$htmlhint_can_scan" = true ] && [ ! -x "$htmlhint_bin" ]; then + echo "::warning::HTMLHint binary missing after install - emitting empty HTMLHint SARIF run." + htmlhint_can_scan=false + fi + + if [ "$htmlhint_can_scan" = false ]; then + emit_empty_sarif + else + "$htmlhint_bin" \ + --config "$htmlhint_config" \ + --format sarif \ + "${files[@]}" > "$raw" || true + if [ ! -s "$raw" ]; then + echo "::warning::HTMLHint scan produced no SARIF output - emitting empty HTMLHint SARIF run." + emit_empty_sarif + fi + fi + fi +fi + +python3 "$RUNNER_DIR/.github/scripts/sigilix_sarif_contract.py" \ + htmlhint "$raw" "$out" --cap "$RESULT_CAP" --ensure-run diff --git a/.github/scripts/run_stylelint.sh b/.github/scripts/run_stylelint.sh new file mode 100644 index 0000000..a15756f --- /dev/null +++ b/.github/scripts/run_stylelint.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${RESULT_CAP:?}" +: "${RUNNER_DIR:?}" +: "${RUNNER_TEMP:?}" +: "${SARIF_DIR:?}" +: "${SOURCE_DIR:?}" +: "${STYLELINT_NPM_INTEGRITY:?}" +: "${STYLELINT_VERSION:?}" + +SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd -P)" +RUNNER_DIR="$(cd "$RUNNER_DIR" && pwd -P)" + +json="$SARIF_DIR/stylelint.json" +out="$SARIF_DIR/stylelint.sarif" +stylelint_config="$RUNNER_DIR/.github/config/stylelint-sigilix.json" +stylelint_install_dir="" +stylelint_bin="" +stylelint_package="" +files_list="" + +mkdir -p "$SARIF_DIR" "$RUNNER_TEMP" +files_list="$(mktemp "$RUNNER_TEMP/stylelint-files.XXXXXX")" + +cleanup_stylelint() { + rm -f "$files_list" + if [ -n "$stylelint_install_dir" ]; then rm -rf "$stylelint_install_dir"; fi +} +trap cleanup_stylelint EXIT + +emit_empty_json() { + printf '[]' > "$json" +} + +verify_package_integrity() { + local actual + actual="sha512-$(openssl dgst -sha512 -binary "$1" | openssl base64 -A)" + [ "$actual" = "$STYLELINT_NPM_INTEGRITY" ] +} + +discover_stylesheet_files() { + find -P . \ + \( -type d \( -name '.git' -o -name 'node_modules' -o -name 'dist' -o -name 'build' \ + -o -name 'coverage' -o -name '.next' -o -name 'out' -o -name 'vendor' \) -prune \) -o \ + \( -type f -name '*.css' -print0 \) +} + +cd "$SOURCE_DIR" +if [ ! -f "$stylelint_config" ]; then + echo "::warning::Stylelint Sigilix config missing at $stylelint_config - emitting empty Stylelint SARIF run." + emit_empty_json +elif ! discover_stylesheet_files > "$files_list"; then + echo "::warning::Stylelint file discovery failed - emitting empty Stylelint SARIF run." + emit_empty_json +else + files=() + while IFS= read -r -d '' file; do + files+=("$file") + done < "$files_list" + if [ "${#files[@]}" -eq 0 ]; then + emit_empty_json + elif [[ ! "$STYLELINT_VERSION" =~ ^[0-9]+[.][0-9]+[.][0-9]+$ ]]; then + echo "::warning::Stylelint version must be a pinned x.y.z version - emitting empty Stylelint SARIF run." + emit_empty_json + elif [[ ! "$STYLELINT_NPM_INTEGRITY" =~ ^sha512-[A-Za-z0-9+/]+={0,2}$ ]]; then + echo "::warning::Stylelint package integrity must be a pinned sha512 value - emitting empty Stylelint SARIF run." + emit_empty_json + else + stylelint_can_scan=true + stylelint_install_dir="$(mktemp -d "$RUNNER_TEMP/stylelint-${STYLELINT_VERSION}.XXXXXX")" + stylelint_bin="$stylelint_install_dir/node_modules/.bin/stylelint" + if ! stylelint_package_json="$(npm pack --json --silent --pack-destination "$stylelint_install_dir" \ + --registry=https://registry.npmjs.org \ + "stylelint@${STYLELINT_VERSION}")"; then + echo "::warning::Stylelint package download failed - emitting empty Stylelint SARIF run." + stylelint_can_scan=false + elif ! stylelint_package="$(PACK_JSON="$stylelint_package_json" python3 - <<'PY' +import json +import os +import sys + +try: + data = json.loads(os.environ["PACK_JSON"]) + if not isinstance(data, list) or len(data) != 1: + raise ValueError("expected one packed package") + filename = data[0].get("filename") + if not isinstance(filename, str) or not filename: + raise ValueError("missing packed filename") + print(filename) +except Exception: + sys.exit(1) +PY +)"; then + echo "::warning::Stylelint package download returned invalid metadata - emitting empty Stylelint SARIF run." + stylelint_can_scan=false + else + package_name="${stylelint_package##*/}" + if [[ ! "$package_name" =~ ^stylelint-[0-9]+[.][0-9]+[.][0-9]+[.]tgz$ ]]; then + echo "::warning::Stylelint package download returned an unexpected filename - emitting empty Stylelint SARIF run." + stylelint_can_scan=false + else + stylelint_package="$stylelint_install_dir/$package_name" + fi + fi + + if [ "$stylelint_can_scan" = true ] && [ ! -f "$stylelint_package" ]; then + echo "::warning::Stylelint package tarball missing after download - emitting empty Stylelint SARIF run." + stylelint_can_scan=false + elif [ "$stylelint_can_scan" = true ] && ! verify_package_integrity "$stylelint_package"; then + echo "::warning::Stylelint package integrity mismatch - emitting empty Stylelint SARIF run." + rm -f "$stylelint_package" + stylelint_can_scan=false + elif [ "$stylelint_can_scan" = true ] && ! npm install --silent --prefix "$stylelint_install_dir" --ignore-scripts --omit=optional \ + --registry=https://registry.npmjs.org --no-audit --no-fund \ + "$stylelint_package" >/dev/null; then + echo "::warning::Stylelint package install failed - emitting empty Stylelint SARIF run." + stylelint_can_scan=false + elif [ "$stylelint_can_scan" = true ] && [ ! -x "$stylelint_bin" ]; then + echo "::warning::Stylelint binary missing after install - emitting empty Stylelint SARIF run." + stylelint_can_scan=false + fi + + if [ "$stylelint_can_scan" = false ]; then + emit_empty_json + else + "$stylelint_bin" \ + "${files[@]}" \ + --config "$stylelint_config" \ + --formatter json \ + --output-file "$json" \ + --allow-empty-input \ + --no-color || true + if [ ! -s "$json" ]; then + echo "::warning::Stylelint scan produced no JSON output - emitting empty Stylelint SARIF run." + emit_empty_json + fi + fi + fi +fi + +python3 "$RUNNER_DIR/.github/scripts/stylelint_to_sarif.py" "$json" "$out" \ + --base-dir "$SOURCE_DIR" --cap "$RESULT_CAP" diff --git a/.github/scripts/run_tflint.sh b/.github/scripts/run_tflint.sh new file mode 100644 index 0000000..31669de --- /dev/null +++ b/.github/scripts/run_tflint.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${RESULT_CAP:?}" +: "${RUNNER_DIR:?}" +: "${RUNNER_TEMP:?}" +: "${SARIF_DIR:?}" +: "${SOURCE_DIR:?}" +: "${TFLINT_LINUX_AMD64_SHA256:?}" +: "${TFLINT_VERSION:?}" + +SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd -P)" +RUNNER_DIR="$(cd "$RUNNER_DIR" && pwd -P)" + +raw="$SARIF_DIR/tflint.raw.sarif" +out="$SARIF_DIR/tflint.sarif" +files_list="" + +mkdir -p "$SARIF_DIR" "$RUNNER_TEMP" +files_list="$(mktemp "$RUNNER_TEMP/tflint-files.XXXXXX")" + +cleanup_tflint() { + rm -f "$files_list" "$RUNNER_TEMP/tflint.zip" "$RUNNER_TEMP/tflint" +} +trap cleanup_tflint EXIT + +emit_empty_sarif() { + printf '{"version":"2.1.0","runs":[]}' > "$raw" +} + +discover_terraform_files() { + find -P . \ + \( -type d \( -name '.git' -o -name 'node_modules' -o -name '.terraform' \) -prune \) -o \ + \( -type f -name '*.tf' -print0 \) +} + +cd "$SOURCE_DIR" +if ! discover_terraform_files > "$files_list"; then + echo "::warning::TFLint file discovery failed - emitting empty TFLint SARIF run." + emit_empty_sarif +elif ! grep -qz . "$files_list"; then + echo "::notice::No Terraform files found - emitting empty TFLint SARIF run." + emit_empty_sarif +elif [[ ! "$TFLINT_VERSION" =~ ^[0-9]+[.][0-9]+[.][0-9]+$ ]]; then + echo "::warning::TFLint version must be a pinned x.y.z version - emitting empty TFLint SARIF run." + emit_empty_sarif +elif [[ ! "$TFLINT_LINUX_AMD64_SHA256" =~ ^[0-9a-f]{64}$ ]]; then + echo "::warning::TFLint checksum must be a pinned SHA256 value - emitting empty TFLint SARIF run." + emit_empty_sarif +elif ! curl -fsSL -o "$RUNNER_TEMP/tflint.zip" \ + "https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/tflint_linux_amd64.zip"; then + echo "::warning::TFLint download failed - emitting empty TFLint SARIF run." + emit_empty_sarif +elif ! printf '%s %s\n' "$TFLINT_LINUX_AMD64_SHA256" "$RUNNER_TEMP/tflint.zip" | sha256sum -c --strict -; then + echo "::warning::TFLint checksum mismatch - emitting empty TFLint SARIF run." + emit_empty_sarif +elif ! unzip -q -o "$RUNNER_TEMP/tflint.zip" -d "$RUNNER_TEMP"; then + echo "::warning::TFLint unzip failed - emitting empty TFLint SARIF run." + emit_empty_sarif +elif [ ! -x "$RUNNER_TEMP/tflint" ]; then + echo "::warning::TFLint binary missing or not executable after extract - emitting empty TFLint SARIF run." + emit_empty_sarif +elif ! tflint_version="$("$RUNNER_TEMP/tflint" --version 2>/dev/null)"; then + echo "::warning::TFLint version check failed - emitting empty TFLint SARIF run." + emit_empty_sarif +elif ! printf '%s\n' "$tflint_version" | grep -q "^TFLint version ${TFLINT_VERSION}\\b"; then + echo "::warning::TFLint installed version mismatch - emitting empty TFLint SARIF run." + emit_empty_sarif +else + "$RUNNER_TEMP/tflint" --recursive --format sarif > "$raw" || true + if [ ! -s "$raw" ]; then + echo "::warning::TFLint scan produced no SARIF output - emitting empty TFLint SARIF run." + emit_empty_sarif + fi +fi + +python3 "$RUNNER_DIR/.github/scripts/sigilix_sarif_contract.py" \ + tflint "$raw" "$out" --cap "$RESULT_CAP" --ensure-run diff --git a/.github/scripts/run_tsc.sh b/.github/scripts/run_tsc.sh index c23b327..4a772c1 100644 --- a/.github/scripts/run_tsc.sh +++ b/.github/scripts/run_tsc.sh @@ -76,7 +76,13 @@ else echo "::warning::TypeScript package download failed - emitting empty TypeScript SARIF run." tsc_can_scan=false else - tsc_package="$tsc_install_dir/${tsc_package##*/}" + package_name="${tsc_package##*/}" + if [[ ! "$package_name" =~ ^typescript-[0-9]+[.][0-9]+[.][0-9]+[.]tgz$ ]]; then + echo "::warning::TypeScript package download returned an unexpected filename - emitting empty TypeScript SARIF run." + tsc_can_scan=false + else + tsc_package="$tsc_install_dir/$package_name" + fi fi if [ "$tsc_can_scan" = true ] && [ ! -f "$tsc_package" ]; then @@ -84,6 +90,7 @@ else tsc_can_scan=false elif [ "$tsc_can_scan" = true ] && ! verify_package_integrity "$tsc_package"; then echo "::warning::TypeScript package integrity mismatch - emitting empty TypeScript SARIF run." + rm -f "$tsc_package" tsc_can_scan=false elif [ "$tsc_can_scan" = true ] && ! npm install --silent --prefix "$tsc_install_dir" --ignore-scripts --omit=optional \ --registry=https://registry.npmjs.org --no-audit --no-fund \ diff --git a/.github/scripts/sigilix_sarif_contract.py b/.github/scripts/sigilix_sarif_contract.py index 275b733..c8b5b59 100644 --- a/.github/scripts/sigilix_sarif_contract.py +++ b/.github/scripts/sigilix_sarif_contract.py @@ -12,7 +12,9 @@ "tsc", "ruff", "pylint", + "flake8", "knip", + "golangci-lint", "actionlint", "shellcheck", "gitleaks", @@ -26,6 +28,8 @@ "biome", "oxlint", "ast-grep", + "htmlhint", + "stylelint", "yamllint", "markdownlint", "dotenv-linter", @@ -38,7 +42,9 @@ "tsc": "TypeScript Compiler", "ruff": "Ruff", "pylint": "Pylint", + "flake8": "Flake8", "knip": "Knip", + "golangci-lint": "golangci-lint", "actionlint": "actionlint", "shellcheck": "ShellCheck", "gitleaks": "gitleaks", @@ -52,6 +58,8 @@ "biome": "Biome", "oxlint": "Oxlint", "ast-grep": "ast-grep", + "htmlhint": "HTMLHint", + "stylelint": "Stylelint", "yamllint": "YAMLlint", "markdownlint": "markdownlint", "dotenv-linter": "dotenv-linter", diff --git a/.github/scripts/sigilix_sarif_test.py b/.github/scripts/sigilix_sarif_test.py index 47e0565..dd3315b 100644 --- a/.github/scripts/sigilix_sarif_test.py +++ b/.github/scripts/sigilix_sarif_test.py @@ -427,8 +427,10 @@ def test_manifest_treats_non_object_sarif_runs_as_invalid_output(self): def test_manifest_records_missing_outputs_for_enabled_tools(self): expected_outputs = { - **NEXT_BATCH_TOOL_OUTPUTS, + **OPT_IN_SECURITY_TOOL_OUTPUTS, + **TERRAFORM_TOOL_OUTPUTS, **LANGUAGE_SARIF_TOOL_OUTPUTS, + **LANGUAGE_CONVERTER_TOOL_OUTPUTS, **CONFIG_TOOL_OUTPUTS, **CI_SECURITY_TOOL_OUTPUTS, **CONTAINER_TOOL_OUTPUTS, @@ -467,14 +469,28 @@ def test_contract_cli_attaches_metadata_for_new_native_language_tools(self): self.assertEqual(driver["properties"]["sigilixToolId"], tool_id) -NEXT_BATCH_TOOL_OUTPUTS = { +OPT_IN_SECURITY_TOOL_OUTPUTS = { "checkov": "checkov.sarif", "trivy": "trivy.sarif", "trufflehog": "trufflehog.sarif", +} + +TERRAFORM_TOOL_OUTPUTS = { "tflint": "tflint.sarif", } -LANGUAGE_SARIF_TOOL_OUTPUTS = {"biome": "biome.sarif", "oxlint": "oxlint.sarif", "ast-grep": "ast-grep.sarif"} +LANGUAGE_SARIF_TOOL_OUTPUTS = { + "biome": "biome.sarif", + "oxlint": "oxlint.sarif", + "ast-grep": "ast-grep.sarif", + "golangci-lint": "golangci-lint.sarif", + "htmlhint": "htmlhint.sarif", +} + +LANGUAGE_CONVERTER_TOOL_OUTPUTS = { + "flake8": "flake8.sarif", + "stylelint": "stylelint.sarif", +} CONFIG_TOOL_OUTPUTS = {"yamllint": "yamllint.sarif", "markdownlint": "markdownlint.sarif", "dotenv-linter": "dotenv-linter.sarif", "checkmake": "checkmake.sarif"} @@ -497,8 +513,10 @@ def test_contract_cli_attaches_metadata_for_new_native_language_tools(self): "shellcheck": "shellcheck.sarif", "gitleaks": "gitleaks.sarif", "osv-scanner": "osv.sarif", - **NEXT_BATCH_TOOL_OUTPUTS, + **OPT_IN_SECURITY_TOOL_OUTPUTS, + **TERRAFORM_TOOL_OUTPUTS, **LANGUAGE_SARIF_TOOL_OUTPUTS, + **LANGUAGE_CONVERTER_TOOL_OUTPUTS, **CONFIG_TOOL_OUTPUTS, **CI_SECURITY_TOOL_OUTPUTS, **CONTAINER_TOOL_OUTPUTS, @@ -584,12 +602,12 @@ def test_tool_manifest_rejects_missing_known_tool_ids(self): def test_tool_output_groups_are_disjoint(self): legacy_tools = {"semgrep", "eslint", "ruff", "actionlint", "shellcheck", "gitleaks", "osv-scanner"} - self.assertFalse(legacy_tools & set(NEXT_BATCH_TOOL_OUTPUTS)) + self.assertFalse(legacy_tools & set(OPT_IN_SECURITY_TOOL_OUTPUTS)) self.assertFalse(legacy_tools & set(LANGUAGE_SARIF_TOOL_OUTPUTS)) - self.assertFalse(set(NEXT_BATCH_TOOL_OUTPUTS) & set(LANGUAGE_SARIF_TOOL_OUTPUTS)) - self.assertNotIn("zizmor", NEXT_BATCH_TOOL_OUTPUTS) + self.assertFalse(set(OPT_IN_SECURITY_TOOL_OUTPUTS) & set(LANGUAGE_SARIF_TOOL_OUTPUTS)) + self.assertNotIn("zizmor", OPT_IN_SECURITY_TOOL_OUTPUTS) self.assertIn("zizmor", CI_SECURITY_TOOL_OUTPUTS) - self.assertNotIn("hadolint", NEXT_BATCH_TOOL_OUTPUTS) + self.assertNotIn("hadolint", OPT_IN_SECURITY_TOOL_OUTPUTS) self.assertIn("hadolint", CONTAINER_TOOL_OUTPUTS) def test_workflow_uses_static_tool_manifest_for_manifest_and_merge(self): @@ -602,11 +620,10 @@ def test_workflow_uses_static_tool_manifest_for_manifest_and_merge(self): self.assertNotIn('python3 - "$TOOL_MANIFEST"', text) self.assertNotIn("jq -r", text) - def test_opt_in_tool_inputs_are_default_off_except_biome_and_oxlint(self): + def test_remaining_opt_in_security_tool_inputs_are_default_off(self): text = self.workflow_text() - self.assertTrue({"biome", "oxlint", "ast-grep"} <= set({**NEXT_BATCH_TOOL_OUTPUTS, **LANGUAGE_SARIF_TOOL_OUTPUTS})) - for tool_id in set({**NEXT_BATCH_TOOL_OUTPUTS, **LANGUAGE_SARIF_TOOL_OUTPUTS}) - {"biome", "oxlint", "ast-grep"}: + for tool_id in OPT_IN_SECURITY_TOOL_OUTPUTS: self.assertRegex( text, rf"\n {re.escape(tool_id)}:\n(?: .+\n)+? default: false\n", @@ -644,8 +661,10 @@ def test_hadolint_is_default_on_for_dockerfile_feedback(self): def test_catalog_tool_outputs_are_manifested_and_merged(self): rows = {row["id"]: row for row in self.tool_manifest()} expected_outputs = { - **NEXT_BATCH_TOOL_OUTPUTS, + **OPT_IN_SECURITY_TOOL_OUTPUTS, + **TERRAFORM_TOOL_OUTPUTS, **LANGUAGE_SARIF_TOOL_OUTPUTS, + **LANGUAGE_CONVERTER_TOOL_OUTPUTS, **CONFIG_TOOL_OUTPUTS, **CI_SECURITY_TOOL_OUTPUTS, **CONTAINER_TOOL_OUTPUTS, @@ -837,120 +856,6 @@ def test_shellcheck_json1_and_legacy_array_convert_to_sarif(self): self.assert_sigilix_properties(legacy_document, "shellcheck") self.assertEqual(legacy_document["runs"][0]["results"][0]["level"], "note") - def test_trufflehog_json_lines_convert_to_sarif(self): - from trufflehog_to_sarif import convert_trufflehog_json - - document = convert_trufflehog_json( - json.dumps( - { - "SourceMetadata": {"Data": {"Filesystem": {"file": "/repo/secrets.env", "line": 4}}}, - "DetectorName": "AWS", - "Verified": True, - "Raw": "AKIA_SHOULD_NOT_APPEAR", - "Redacted": "AKIA********", - "ExtraData": {"account": "SHOULD_NOT_APPEAR"}, - "StructuredData": {"token": "STRUCTURED_SHOULD_NOT_APPEAR"}, - } - ) - + "\n", - base_dir="/repo", - ) - - self.assert_sigilix_properties(document, "trufflehog") - result = document["runs"][0]["results"][0] - self.assertEqual(result["ruleId"], "AWS") - self.assertEqual(result["level"], "error") - self.assertEqual(result["message"]["text"], "TruffleHog found AWS secret") - self.assertEqual(result["properties"], {"trufflehogVerified": True}) - self.assertNotIn("AKIA", json.dumps(document)) - self.assertNotIn("SHOULD_NOT_APPEAR", json.dumps(document)) - self.assertNotIn("STRUCTURED_SHOULD_NOT_APPEAR", json.dumps(document)) - self.assertEqual(result["locations"][0]["physicalLocation"]["artifactLocation"]["uri"], "secrets.env") - self.assertEqual(result["locations"][0]["physicalLocation"]["region"]["startLine"], 4) - - def test_trufflehog_unverified_findings_are_warning_without_secret_dependent_dedupe(self): - from trufflehog_to_sarif import convert_trufflehog_json - - finding = { - "SourceMetadata": {"Data": {"Filesystem": {"file": "/repo/secrets.env", "line": 4}}}, - "DetectorName": "AWS", - "Verified": False, - "Raw": "AKIA_DUPLICATE_SECRET", - } - document = convert_trufflehog_json("\n".join([json.dumps(finding), json.dumps(finding)]), base_dir="/repo") - - results = document["runs"][0]["results"] - self.assertEqual(len(results), 2) - self.assertEqual(results[0]["level"], "warning") - self.assertEqual(results[0]["message"]["text"], "TruffleHog found AWS secret") - self.assertEqual(results[0]["properties"], {"trufflehogVerified": False}) - self.assertNotIn("AKIA_DUPLICATE_SECRET", json.dumps(document)) - - metadata_only = dict(finding) - metadata_only.pop("Raw") - document = convert_trufflehog_json( - "\n".join([json.dumps(metadata_only), json.dumps(metadata_only)]), - base_dir="/repo", - ) - self.assertEqual(len(document["runs"][0]["results"]), 1) - - whitespace_raw = dict(finding, Raw=" ") - document = convert_trufflehog_json("\n".join([json.dumps(whitespace_raw), json.dumps(whitespace_raw)]), base_dir="/repo") - self.assertEqual(len(document["runs"][0]["results"]), 1) - - numeric_raw = dict(finding, Raw=0) - document = convert_trufflehog_json("\n".join([json.dumps(numeric_raw), json.dumps(numeric_raw)]), base_dir="/repo") - self.assertEqual(len(document["runs"][0]["results"]), 2) - - def test_trufflehog_metadata_dedupe_prefers_verified_finding(self): - from trufflehog_to_sarif import convert_trufflehog_json - - base = {"SourceMetadata": {"Data": {"Filesystem": {"file": "/repo/secrets.env", "line": 4}}}, "DetectorName": "AWS"} - document = convert_trufflehog_json( - "\n".join([json.dumps(dict(base, Verified=False)), json.dumps(dict(base, Verified="true"))]), - base_dir="/repo", - ) - - result = document["runs"][0]["results"][0] - self.assertEqual(len(document["runs"][0]["results"]), 1) - self.assertEqual(result["level"], "error") - self.assertEqual(result["properties"], {"trufflehogVerified": True}) - - other = {"SourceMetadata": {"Data": {"Filesystem": {"file": "/repo/other.env", "line": 8}}}, "DetectorName": "GCP"} - document = convert_trufflehog_json("\n".join([json.dumps(base), json.dumps(other)]), base_dir="/repo") - self.assertEqual([result["ruleId"] for result in document["runs"][0]["results"]], ["AWS", "GCP"]) - - def test_trufflehog_ndjson_warns_on_non_object_lines(self): - from trufflehog_to_sarif import convert_trufflehog_json - - payload = "\n".join( - [ - json.dumps({"SourceMetadata": {"Data": {"Filesystem": {"file": "/repo/first.env", "line": 1}}}, "DetectorName": "AWS"}), - json.dumps(["unexpected"]), - json.dumps({"SourceMetadata": {"Data": {"Git": {"file": "/repo/second.env", "line": 2}}}, "DetectorName": "GitHub"}), - ] - ) - stderr = io.StringIO() - - with contextlib.redirect_stderr(stderr): - document = convert_trufflehog_json(payload, base_dir="/repo") - - self.assert_sigilix_properties(document, "trufflehog") - self.assertEqual([result["ruleId"] for result in document["runs"][0]["results"]], ["AWS", "GitHub"]) - self.assertIn("skipped a non-object JSON line", stderr.getvalue()) - - def test_trufflehog_missing_line_omits_sarif_region(self): - from trufflehog_to_sarif import convert_trufflehog_json - - document = convert_trufflehog_json( - json.dumps({"SourceMetadata": {"Data": {"Filesystem": {"file": "/repo/secrets.env"}}}, "DetectorName": "AWS"}), - base_dir="/repo", - ) - - result = document["runs"][0]["results"][0] - self.assertEqual(result["locations"][0]["physicalLocation"]["artifactLocation"]["uri"], "secrets.env") - self.assertNotIn("region", result["locations"][0]["physicalLocation"]) - def test_yamllint_parsable_output_converts_to_sarif(self): from yamllint_to_sarif import convert_yamllint_output diff --git a/.github/scripts/stylelint_to_sarif.py b/.github/scripts/stylelint_to_sarif.py new file mode 100644 index 0000000..1598ceb --- /dev/null +++ b/.github/scripts/stylelint_to_sarif.py @@ -0,0 +1,66 @@ +import argparse +import sys + +from sarif_converter_common import load_json_file, make_document, make_result, write_json_file + + +STYLELINT_TOOL_ID = "stylelint" +STYLELINT_TOOL_NAME = "Stylelint" +STYLELINT_INFORMATION_URI = "https://stylelint.io/" +_LEVELS = {"error": "error", "warning": "warning", "info": "note"} + + +def convert_stylelint_json(data, base_dir=".", cap=None): + results = [] + for entry in _list_or_empty(data): + if not isinstance(entry, dict): + continue + source = str(entry.get("source") or "") + for warning in _list_or_empty(entry.get("warnings")): + if not isinstance(warning, dict): + continue + results.append(_warning_to_result(source, warning, base_dir=base_dir)) + return make_document(STYLELINT_TOOL_NAME, STYLELINT_TOOL_ID, results, information_uri=STYLELINT_INFORMATION_URI, cap=cap) + + +def _warning_to_result(source, warning, base_dir="."): + rule = str(warning.get("rule") or "stylelint").strip() or "stylelint" + severity = str(warning.get("severity") or "").lower() + line = warning.get("line") + column = warning.get("column") + return make_result( + rule, + _LEVELS.get(severity, "warning"), + str(warning.get("text") or ""), + source, + line=line, + column=column, + end_line=warning.get("endLine") if _is_positive_int(line) else None, + end_column=warning.get("endColumn") if _is_positive_int(column) else None, + base_dir=base_dir, + ) + + +def _list_or_empty(value): + return value if isinstance(value, list) else [] + + +def _is_positive_int(value): + return not isinstance(value, bool) and isinstance(value, int) and value > 0 + + +def _main(argv): + parser = argparse.ArgumentParser(description="Convert Stylelint JSON output to Sigilix SARIF.") + parser.add_argument("input") + parser.add_argument("output") + parser.add_argument("--base-dir", default=".") + parser.add_argument("--cap", type=int) + args = parser.parse_args(argv) + + document = convert_stylelint_json(load_json_file(args.input), base_dir=args.base_dir, cap=args.cap) + write_json_file(args.output, document) + return 0 + + +if __name__ == "__main__": + sys.exit(_main(sys.argv[1:])) diff --git a/.github/scripts/trufflehog_converter_test.py b/.github/scripts/trufflehog_converter_test.py new file mode 100644 index 0000000..211cc62 --- /dev/null +++ b/.github/scripts/trufflehog_converter_test.py @@ -0,0 +1,126 @@ +import contextlib +import io +import json +import unittest + +from sigilix_sarif_contract import SIGILIX_SCHEMA_VERSION, SIGILIX_SOURCE +from trufflehog_to_sarif import convert_trufflehog_json + + +class TrufflehogConverterTest(unittest.TestCase): + def assert_sigilix_properties(self, document, tool_id): + self.assertEqual(document["version"], "2.1.0") + self.assertEqual(len(document["runs"]), 1) + properties = document["runs"][0]["tool"]["driver"]["properties"] + self.assertEqual(properties["sigilixSchemaVersion"], SIGILIX_SCHEMA_VERSION) + self.assertEqual(properties["sigilixToolId"], tool_id) + self.assertEqual(properties["sigilixSource"], SIGILIX_SOURCE) + self.assertNotIn("sigilixRoleHints", properties) + + def test_trufflehog_json_lines_convert_to_sarif(self): + document = convert_trufflehog_json( + json.dumps( + { + "SourceMetadata": {"Data": {"Filesystem": {"file": "/repo/secrets.env", "line": 4}}}, + "DetectorName": "AWS", + "Verified": True, + "Raw": "AKIA_SHOULD_NOT_APPEAR", + "Redacted": "AKIA********", + "ExtraData": {"account": "SHOULD_NOT_APPEAR"}, + "StructuredData": {"token": "STRUCTURED_SHOULD_NOT_APPEAR"}, + } + ) + + "\n", + base_dir="/repo", + ) + + self.assert_sigilix_properties(document, "trufflehog") + result = document["runs"][0]["results"][0] + self.assertEqual(result["ruleId"], "AWS") + self.assertEqual(result["level"], "error") + self.assertEqual(result["message"]["text"], "TruffleHog found AWS secret") + self.assertEqual(result["properties"], {"trufflehogVerified": True}) + self.assertNotIn("AKIA", json.dumps(document)) + self.assertNotIn("SHOULD_NOT_APPEAR", json.dumps(document)) + self.assertNotIn("STRUCTURED_SHOULD_NOT_APPEAR", json.dumps(document)) + self.assertEqual(result["locations"][0]["physicalLocation"]["artifactLocation"]["uri"], "secrets.env") + self.assertEqual(result["locations"][0]["physicalLocation"]["region"]["startLine"], 4) + + def test_trufflehog_unverified_findings_are_warning_without_secret_dependent_dedupe(self): + finding = { + "SourceMetadata": {"Data": {"Filesystem": {"file": "/repo/secrets.env", "line": 4}}}, + "DetectorName": "AWS", + "Verified": False, + "Raw": "AKIA_DUPLICATE_SECRET", + } + document = convert_trufflehog_json("\n".join([json.dumps(finding), json.dumps(finding)]), base_dir="/repo") + + results = document["runs"][0]["results"] + self.assertEqual(len(results), 2) + self.assertEqual(results[0]["level"], "warning") + self.assertEqual(results[0]["message"]["text"], "TruffleHog found AWS secret") + self.assertEqual(results[0]["properties"], {"trufflehogVerified": False}) + self.assertNotIn("AKIA_DUPLICATE_SECRET", json.dumps(document)) + + metadata_only = dict(finding) + metadata_only.pop("Raw") + document = convert_trufflehog_json( + "\n".join([json.dumps(metadata_only), json.dumps(metadata_only)]), + base_dir="/repo", + ) + self.assertEqual(len(document["runs"][0]["results"]), 1) + + whitespace_raw = dict(finding, Raw=" ") + document = convert_trufflehog_json("\n".join([json.dumps(whitespace_raw), json.dumps(whitespace_raw)]), base_dir="/repo") + self.assertEqual(len(document["runs"][0]["results"]), 1) + + numeric_raw = dict(finding, Raw=0) + document = convert_trufflehog_json("\n".join([json.dumps(numeric_raw), json.dumps(numeric_raw)]), base_dir="/repo") + self.assertEqual(len(document["runs"][0]["results"]), 2) + + def test_trufflehog_metadata_dedupe_prefers_verified_finding(self): + base = {"SourceMetadata": {"Data": {"Filesystem": {"file": "/repo/secrets.env", "line": 4}}}, "DetectorName": "AWS"} + document = convert_trufflehog_json( + "\n".join([json.dumps(dict(base, Verified=False)), json.dumps(dict(base, Verified="true"))]), + base_dir="/repo", + ) + + result = document["runs"][0]["results"][0] + self.assertEqual(len(document["runs"][0]["results"]), 1) + self.assertEqual(result["level"], "error") + self.assertEqual(result["properties"], {"trufflehogVerified": True}) + + other = {"SourceMetadata": {"Data": {"Filesystem": {"file": "/repo/other.env", "line": 8}}}, "DetectorName": "GCP"} + document = convert_trufflehog_json("\n".join([json.dumps(base), json.dumps(other)]), base_dir="/repo") + self.assertEqual([result["ruleId"] for result in document["runs"][0]["results"]], ["AWS", "GCP"]) + + def test_trufflehog_ndjson_warns_on_non_object_lines(self): + payload = "\n".join( + [ + json.dumps({"SourceMetadata": {"Data": {"Filesystem": {"file": "/repo/first.env", "line": 1}}}, "DetectorName": "AWS"}), + json.dumps(["unexpected"]), + json.dumps({"SourceMetadata": {"Data": {"Git": {"file": "/repo/second.env", "line": 2}}}, "DetectorName": "GitHub"}), + ] + ) + stderr = io.StringIO() + + with contextlib.redirect_stderr(stderr): + document = convert_trufflehog_json(payload, base_dir="/repo") + + self.assert_sigilix_properties(document, "trufflehog") + self.assertEqual([result["ruleId"] for result in document["runs"][0]["results"]], ["AWS", "GitHub"]) + self.assertIn("skipped a non-object JSON line", stderr.getvalue()) + + def test_trufflehog_missing_line_omits_sarif_region(self): + document = convert_trufflehog_json( + json.dumps({"SourceMetadata": {"Data": {"Filesystem": {"file": "/repo/secrets.env"}}}, "DetectorName": "AWS"}), + base_dir="/repo", + ) + + result = document["runs"][0]["results"][0] + self.assertEqual(result["locations"][0]["physicalLocation"]["artifactLocation"]["uri"], "secrets.env") + self.assertNotIn("region", result["locations"][0]["physicalLocation"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 162f534..4b06832 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,8 @@ jobs: python3 .github/scripts/eslint_ts_workflow_test.py python3 .github/scripts/tsc_workflow_test.py python3 .github/scripts/pylint_workflow_test.py + python3 .github/scripts/trufflehog_converter_test.py + python3 .github/scripts/language_config_tools_workflow_test.py python3 .github/scripts/knip_workflow_test.py python3 .github/scripts/biome_workflow_test.py python3 .github/scripts/docs_config_tools_test.py diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index f3adc37..c45e42c 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -62,11 +62,21 @@ on: required: false default: true type: boolean + flake8: + description: "Run Sigilix-controlled Flake8 checks when a .flake8 marker is present." + required: false + default: true + type: boolean knip: description: "Run Knip with Sigilix-owned JavaScript and TypeScript dependency-resolution checks." required: false default: true type: boolean + golangci-lint: + description: "Run golangci-lint with Sigilix-controlled standard Go linters." + required: false + default: true + type: boolean actionlint: description: "Run actionlint on GitHub Actions workflow files." required: false @@ -115,7 +125,7 @@ on: tflint: description: "Run TFLint on Terraform files and merge its SARIF." required: false - default: false + default: true type: boolean biome: description: "Run Biome on web source files and merge its SARIF." @@ -132,6 +142,16 @@ on: required: false default: true type: boolean + htmlhint: + description: "Run HTMLHint on HTML files with Sigilix-controlled correctness rules." + required: false + default: true + type: boolean + stylelint: + description: "Run Stylelint on CSS files with Sigilix-controlled correctness rules." + required: false + default: true + type: boolean yamllint: description: "Run YAMLlint on YAML config files with relaxed defaults." required: false @@ -191,8 +211,13 @@ jobs: ESLINT_PLUGIN_SECURITY_VERSION: "4.0.1" ESLINT_PLUGIN_UNICORN_VERSION: "65.0.1" ESLINT_VERSION: "10.4.1" + FLAKE8_VERSION: "7.3.0" GITLEAKS_VERSION: "8.21.2" + GOLANGCI_LINT_LINUX_AMD64_SHA256: "8df580d2670fed8fa984aac0507099af8df275e665215f5c7a2ae3943893a553" + GOLANGCI_LINT_VERSION: "2.12.2" HADOLINT_VERSION: "2.14.0" + HTMLHINT_NPM_INTEGRITY: "sha512-PweWSPA1Pb+AVFIOSpIGu5KhLdmtk/uf/0CpjvrDf6XUWmdTyqUljlylwSxQ0AWLvPGcBxK2n8uISsI4lCOkBQ==" + HTMLHINT_VERSION: "1.9.2" KNIP_VERSION: "6.16.1" MARKDOWNLINT_VERSION: "0.48.0" NODE_VERSION: "22.13.0" @@ -206,6 +231,9 @@ jobs: SARIF_BYTE_CAP: ${{ inputs.sarif-byte-cap }} SEMGREP_VERSION: "1.166.0" SHELLCHECK_VERSION: "0.11.0" + STYLELINT_NPM_INTEGRITY: "sha512-KIlzWXMHUvgfPUR0R7TK3H80yCIi0uoivUwf+6Az4yrHJD1Q3c1qIkh/H5Z0i/K3QXgtq/UMEkWyBUSUwnpnOg==" + STYLELINT_VERSION: "17.12.0" + TFLINT_LINUX_AMD64_SHA256: "8441a7d97df20431f19c9b9d27ff4c63e308c964e86660bc7cc0cf7bbe0725e8" TFLINT_VERSION: "0.63.1" TRIVY_VERSION: "0.71.0" TRUFFLEHOG_VERSION: "3.95.5" @@ -304,6 +332,13 @@ jobs: cd "$SOURCE_DIR" bash "$RUNNER_DIR/.github/scripts/run_pylint.sh" + - name: Run Flake8 to SARIF + if: ${{ inputs.flake8 }} + run: | + set -euo pipefail + cd "$SOURCE_DIR" + bash "$RUNNER_DIR/.github/scripts/run_flake8.sh" + - name: Run Knip to SARIF if: ${{ inputs.knip }} run: | @@ -311,6 +346,13 @@ jobs: cd "$SOURCE_DIR" bash "$RUNNER_DIR/.github/scripts/run_knip.sh" + - name: Run golangci-lint to SARIF + if: ${{ inputs.golangci-lint }} + run: | + set -euo pipefail + cd "$SOURCE_DIR" + bash "$RUNNER_DIR/.github/scripts/run_golangci_lint.sh" + - name: Run actionlint to SARIF if: ${{ inputs.actionlint }} run: | @@ -668,33 +710,7 @@ jobs: run: | set -euo pipefail cd "$SOURCE_DIR" - raw="$SARIF_DIR/tflint.raw.sarif" - out="$SARIF_DIR/tflint.sarif" - if ! curl -fsSL -o "$RUNNER_TEMP/tflint.zip" \ - "https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/tflint_linux_amd64.zip"; then - echo "::warning::tflint download failed - manifest will record missing output." - else - if ! unzip -q -o "$RUNNER_TEMP/tflint.zip" -d "$RUNNER_TEMP"; then - echo "::warning::tflint unzip failed - manifest will record missing output." - else - files_list="$RUNNER_TEMP/tflint-files" - if ! find . -type f -name '*.tf' \ - -not -path './.git/*' -not -path './node_modules/*' -print0 > "$files_list"; then - echo "::warning::tflint file discovery failed - manifest will record missing output." - else - mapfile -d '' files < "$files_list" - if [ "${#files[@]}" -eq 0 ]; then - printf '{"version":"2.1.0","runs":[]}' > "$raw" - else - "$RUNNER_TEMP/tflint" --recursive --format sarif > "$raw" || true - if [ ! -s "$raw" ]; then printf '{"version":"2.1.0","runs":[]}' > "$raw"; fi - fi - python3 "$RUNNER_DIR/.github/scripts/sigilix_sarif_contract.py" \ - tflint "$raw" "$out" --cap "$RESULT_CAP" --ensure-run \ - || echo "::warning::tflint SARIF normalization failed - manifest will record missing output." - fi - fi - fi + bash "$RUNNER_DIR/.github/scripts/run_tflint.sh" - name: Run Biome to SARIF if: ${{ inputs.biome }} @@ -828,6 +844,20 @@ jobs: cd "$SOURCE_DIR" bash "$RUNNER_DIR/.github/scripts/run_ast_grep.sh" + - name: Run HTMLHint to SARIF + if: ${{ inputs.htmlhint }} + run: | + set -euo pipefail + cd "$SOURCE_DIR" + bash "$RUNNER_DIR/.github/scripts/run_htmlhint.sh" + + - name: Run Stylelint to SARIF + if: ${{ inputs.stylelint }} + run: | + set -euo pipefail + cd "$SOURCE_DIR" + bash "$RUNNER_DIR/.github/scripts/run_stylelint.sh" + - name: Build scan manifest env: SEMGREP_ENABLED: ${{ inputs.semgrep }} @@ -835,7 +865,9 @@ jobs: TSC_ENABLED: ${{ inputs.tsc }} RUFF_ENABLED: ${{ inputs.ruff }} PYLINT_ENABLED: ${{ inputs.pylint }} + FLAKE8_ENABLED: ${{ inputs.flake8 }} KNIP_ENABLED: ${{ inputs.knip }} + GOLANGCI_LINT_ENABLED: ${{ inputs.golangci-lint }} ACTIONLINT_ENABLED: ${{ inputs.actionlint }} SHELLCHECK_ENABLED: ${{ inputs.shellcheck }} GITLEAKS_ENABLED: ${{ inputs.gitleaks }} @@ -849,6 +881,8 @@ jobs: BIOME_ENABLED: ${{ inputs.biome }} OXLINT_ENABLED: ${{ inputs.oxlint }} AST_GREP_ENABLED: ${{ inputs.ast-grep }} + HTMLHINT_ENABLED: ${{ inputs.htmlhint }} + STYLELINT_ENABLED: ${{ inputs.stylelint }} YAMLLINT_ENABLED: ${{ inputs.yamllint }} MARKDOWNLINT_ENABLED: ${{ inputs.markdownlint }} DOTENV_LINTER_ENABLED: ${{ inputs.dotenv-linter }} diff --git a/README.md b/README.md index 9abbe83..a131ff7 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ Current staged catalog: | TypeScript Compiler | on | Converts `tsc --noEmit` diagnostics to SARIF when TypeScript configs are present. Bare external dependency-resolution noise is filtered, path-like unresolved imports stay visible as review signal, and declaration-file library checks are skipped to keep dependency noise bounded. | | Ruff | on | Native SARIF with Sigilix metadata. | | Pylint | on | Converts Pylint JSON output to SARIF. Uses a Sigilix-owned high-confidence profile: Pylint fatal/error checks only, with caller config ignored and dependency-sensitive `import-error`/`no-member` disabled. | +| Flake8 | on | Converts Flake8 text output to SARIF when a `.flake8` marker is present. The config content is ignored; Sigilix runs high-confidence PyFlakes/parse checks only to avoid duplicating broad Ruff/Pylint style feedback. | | Knip | on | Converts Knip JSON output to SARIF. Uses a Sigilix-owned JavaScript/TypeScript profile for unresolved imports, unlisted dependencies, and missing package-script binaries; broad unused-export/file reports are not enabled. | +| golangci-lint | on | Native SARIF with Sigilix metadata for Go repositories. Uses a runner-controlled standard linter profile and skips caller golangci config/plugins. | | actionlint | on | Converts actionlint JSON to SARIF for GitHub Actions workflows. | | ShellCheck | on | Converts ShellCheck `json1` output to SARIF. | | YAMLlint | on | Converts YAMLlint parsable output to SARIF with relaxed defaults for config feedback. | @@ -37,10 +39,12 @@ Current staged catalog: | TruffleHog | off | Converts TruffleHog JSON output to SARIF. Secret verification is disabled in CI to avoid provider calls with discovered credentials; unverified or unverifiable hits are warning-level and metadata-only duplicates are collapsed without comparing secret values. | | zizmor | on | Native SARIF with Sigilix metadata for GitHub Actions security scanning. | | Hadolint | on | Native SARIF with Sigilix metadata for Dockerfile linting. | -| TFLint | off | Native SARIF with Sigilix metadata. Opt-in Terraform linting. | +| TFLint | on | Native SARIF with Sigilix metadata. Runs only when Terraform files are present. | | Biome | on | Native SARIF with Sigilix metadata. Default-on Sigilix-controlled correctness linting for JS/TS and JSON; uses runner-owned config, bypasses caller ignore files, and skips common generated-output directories. | | Oxlint | on | Native SARIF with Sigilix metadata. Default-on Sigilix-controlled correctness linting for JavaScript and TypeScript; uses runner-owned config, disables caller Oxlint config and ignore files, skips common generated-output directories, and checks pinned npm package integrity before scanning. | | ast-grep | on | Native SARIF with Sigilix metadata. Default-on Sigilix-owned AST rules for high-confidence JavaScript and TypeScript async array logic bugs; verified npm tarballs are installed without package scripts before scanning. Caller ignore files are bypassed; common generated and vendor directories are still excluded. | +| HTMLHint | on | Native SARIF with Sigilix metadata for HTML files. Uses runner-owned structural correctness rules, verifies the pinned npm tarball, and skips common generated-output directories. | +| Stylelint | on | Converts Stylelint JSON output to SARIF for CSS files. Uses runner-owned correctness rules, verifies the pinned npm tarball, and skips common generated-output directories. | > **SIG-107:** `oxlint` now defaults to `true`. Set `oxlint: false` (boolean) in the caller workflow to suppress it. > `biome` now defaults to `true`. Set `biome: false` (boolean) in the caller workflow to suppress it. @@ -48,6 +52,8 @@ Current staged catalog: > `pylint` now defaults to `true`. Set `pylint: false` (boolean) in the caller workflow to suppress it. > `knip` now defaults to `true`. Set `knip: false` (boolean) in the caller workflow to suppress it. > `tsc` now defaults to `true`. Set `tsc: false` (boolean) in the caller workflow to suppress it. +> `flake8`, `golangci-lint`, `htmlhint`, `stylelint`, and `tflint` now default to `true`. +> Set the matching boolean input to `false` in the caller workflow to suppress one of them. > `markdownlint`, `dotenv-linter`, and `checkmake` now default to `true`. Set the matching > boolean input to `false` in the caller workflow to suppress one of them. @@ -89,9 +95,10 @@ a moving ref cannot prove which version of the runner ran. Default-on tool booleans: `semgrep`, `eslint`, `ruff`, `actionlint`, `shellcheck`, `yamllint`, `markdownlint`, `dotenv-linter`, `checkmake`, `gitleaks`, `osv-scanner`, `zizmor`, `hadolint`, -`biome`, `oxlint`, `ast-grep`, `pylint`, `knip`, and `tsc`. +`biome`, `oxlint`, `ast-grep`, `pylint`, `flake8`, `knip`, `golangci-lint`, `htmlhint`, +`stylelint`, `tflint`, and `tsc`. -Default-off opt-in tool booleans: `checkov`, `trivy`, `trufflehog`, and `tflint`. +Default-off opt-in tool booleans: `checkov`, `trivy`, and `trufflehog`. Other useful inputs: