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: