-
Notifications
You must be signed in to change notification settings - Fork 0
feat(SIG-107): add OpenGrep and Brakeman runner tools #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import argparse | ||
| import json | ||
| import sys | ||
| from urllib.parse import urlparse | ||
|
|
||
| from sarif_converter_common import load_json_file, normalize_path, write_json_file | ||
|
|
||
|
|
||
| def _is_external_uri(uri): | ||
| parsed = urlparse(uri) | ||
| return bool(parsed.scheme and parsed.scheme.lower() != "file") | ||
|
|
||
|
|
||
| def _strip_file_uri(uri): | ||
| parsed = urlparse(uri) | ||
| if parsed.scheme.lower() != "file": | ||
| return uri | ||
| return parsed.path or "" | ||
|
|
||
|
|
||
| def _prefixed_path(uri, root, base_dir): | ||
| if not isinstance(uri, str) or not uri or _is_external_uri(uri): | ||
| return uri | ||
| normalized = normalize_path(_strip_file_uri(uri), base_dir=base_dir) | ||
| if root == "." or normalized == root or normalized.startswith(f"{root}/"): | ||
| return normalized | ||
| return f"{root}/{normalized}" | ||
|
|
||
|
|
||
| def _normalize_artifact_locations(value, root, base_dir): | ||
| if isinstance(value, list): | ||
| for item in value: | ||
| _normalize_artifact_locations(item, root, base_dir) | ||
| return | ||
| if not isinstance(value, dict): | ||
| return | ||
| if isinstance(value.get("uri"), str): | ||
| value["uri"] = _prefixed_path(value["uri"], root, base_dir) | ||
| for child in value.values(): | ||
| _normalize_artifact_locations(child, root, base_dir) | ||
|
|
||
|
|
||
| def normalize_brakeman_sarif_paths(document, root, base_dir="."): | ||
| if not isinstance(document, dict): | ||
| raise ValueError("invalid Brakeman SARIF input") | ||
| if document.get("version") != "2.1.0": | ||
| document["version"] = "2.1.0" | ||
| runs = document.get("runs") | ||
| if not isinstance(runs, list): | ||
| document["runs"] = [] | ||
| return document | ||
| root = normalize_path(root or ".", base_dir=base_dir) | ||
| for run in runs: | ||
| if not isinstance(run, dict): | ||
| continue | ||
| driver = run.setdefault("tool", {}).setdefault("driver", {}) | ||
| if root != ".": | ||
| driver["name"] = f"Brakeman ({root})" | ||
| _normalize_artifact_locations(run.get("results"), root, base_dir) | ||
| return document | ||
|
|
||
|
|
||
| def _main(argv): | ||
| parser = argparse.ArgumentParser(description="Normalize Brakeman SARIF paths for nested Rails roots.") | ||
| parser.add_argument("input") | ||
| parser.add_argument("output") | ||
| parser.add_argument("--root", required=True) | ||
| parser.add_argument("--base-dir", default=".") | ||
| args = parser.parse_args(argv) | ||
|
|
||
| document = load_json_file(args.input) | ||
| if not isinstance(document, dict): | ||
| print("invalid Brakeman SARIF input", file=sys.stderr) | ||
| return 1 | ||
| normalized = normalize_brakeman_sarif_paths(document, root=args.root, base_dir=args.base_dir) | ||
| write_json_file(args.output, normalized) | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(_main(sys.argv[1:])) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| : "${BRAKEMAN_GEM_SHA256:?}" | ||
| : "${BRAKEMAN_VERSION:?}" | ||
| : "${RACC_GEM_SHA256:?}" | ||
| : "${RACC_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/brakeman.raw.sarif" | ||
| out="$SARIF_DIR/brakeman.sarif" | ||
| roots_list="" | ||
| brakeman_config="$RUNNER_TEMP/brakeman-sigilix.yml" | ||
| brakeman_ignore="$RUNNER_TEMP/brakeman-ignore.json" | ||
| brakeman_gem_cache="$RUNNER_TEMP/brakeman-gem-cache" | ||
| brakeman_raw_dir="$RUNNER_TEMP/brakeman-raw" | ||
| raw_files=() | ||
|
|
||
| mkdir -p "$SARIF_DIR" "$RUNNER_TEMP" "$brakeman_gem_cache" "$brakeman_raw_dir" | ||
| roots_list="$(mktemp "$RUNNER_TEMP/brakeman-roots.XXXXXX")" | ||
|
|
||
| cleanup_brakeman() { | ||
| rm -f "$roots_list" "$brakeman_config" "$brakeman_ignore" | ||
| rm -rf "$brakeman_gem_cache" "$brakeman_raw_dir" | ||
| } | ||
| trap cleanup_brakeman EXIT | ||
|
|
||
| emit_empty_sarif() { | ||
| printf '{"version":"2.1.0","runs":[]}' > "$raw" | ||
| } | ||
|
|
||
| discover_rails_roots() { | ||
| 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 -path '*/config/application.rb' -print0 \) \ | ||
| | while IFS= read -r -d '' application_file; do | ||
| root="${application_file%/config/application.rb}" | ||
| printf '%s\0' "${root:-.}" | ||
| done \ | ||
| | sort -zu | ||
| } | ||
|
|
||
| write_runner_brakeman_config() { | ||
| printf '%s\n' '--- {}' > "$brakeman_config" | ||
| printf '%s\n' '{"ignored_warnings":[]}' > "$brakeman_ignore" | ||
| } | ||
|
|
||
| fetch_verified_gem() { | ||
| local name="$1" | ||
| local version="$2" | ||
| local checksum="$3" | ||
| local path="$brakeman_gem_cache/${name}-${version}.gem" | ||
| rm -f "$path" | ||
| if ! (cd "$brakeman_gem_cache" && gem fetch --norc --clear-sources --source https://rubygems.org "$name" -v "$version" >/dev/null); then | ||
| echo "::warning::${name} gem fetch failed - manifest will record missing output." | ||
| rm -f "$path" | ||
| return 1 | ||
| fi | ||
| if [ ! -s "$path" ]; then | ||
| echo "::warning::${name} gem package missing after fetch - manifest will record missing output." | ||
| return 1 | ||
| fi | ||
| if ! printf '%s %s\n' "$checksum" "$path" | sha256sum -c --strict -; then | ||
| echo "::warning::${name} gem checksum mismatch - manifest will record missing output." | ||
| return 1 | ||
| fi | ||
| } | ||
|
|
||
| cd "$SOURCE_DIR" | ||
| if ! discover_rails_roots > "$roots_list"; then | ||
| echo "::warning::Brakeman Rails root discovery failed - emitting empty Brakeman SARIF run." | ||
| emit_empty_sarif | ||
| elif ! grep -qz . "$roots_list"; then | ||
| echo "::notice::No Rails roots found - emitting empty Brakeman SARIF run." | ||
| emit_empty_sarif | ||
| elif [[ ! "$BRAKEMAN_VERSION" =~ ^[0-9]+[.][0-9]+[.][0-9]+$ ]]; then | ||
| echo "::warning::Brakeman version must be a pinned x.y.z version - manifest will record missing output." | ||
| elif [[ ! "$RACC_VERSION" =~ ^[0-9]+[.][0-9]+[.][0-9]+$ ]]; then | ||
| echo "::warning::racc version must be a pinned x.y.z version - manifest will record missing output." | ||
| elif [[ ! "$BRAKEMAN_GEM_SHA256" =~ ^[0-9a-f]{64}$ ]] || [[ ! "$RACC_GEM_SHA256" =~ ^[0-9a-f]{64}$ ]]; then | ||
| echo "::warning::Brakeman gem checksums must be pinned SHA256 values - manifest will record missing output." | ||
| elif ! command -v ruby >/dev/null 2>&1 || ! command -v gem >/dev/null 2>&1; then | ||
| echo "::warning::Ruby and gem are required for Brakeman - manifest will record missing output." | ||
| else | ||
| export GEM_HOME="$RUNNER_TEMP/brakeman-gems" | ||
| export GEM_PATH="$GEM_HOME" | ||
| export PATH="$GEM_HOME/bin:$PATH" | ||
| mkdir -p "$GEM_HOME" | ||
| write_runner_brakeman_config | ||
| if ! fetch_verified_gem racc "$RACC_VERSION" "$RACC_GEM_SHA256"; then | ||
| : | ||
| elif ! fetch_verified_gem brakeman "$BRAKEMAN_VERSION" "$BRAKEMAN_GEM_SHA256"; then | ||
| : | ||
| elif ! gem install --norc --local --no-document --install-dir "$GEM_HOME" \ | ||
| "$brakeman_gem_cache/racc-${RACC_VERSION}.gem" \ | ||
| "$brakeman_gem_cache/brakeman-${BRAKEMAN_VERSION}.gem"; then | ||
| echo "::warning::Brakeman install failed - manifest will record missing output." | ||
| elif ! brakeman_version="$(brakeman --version 2>/dev/null)"; then | ||
| echo "::warning::Brakeman version check failed - manifest will record missing output." | ||
| elif ! printf '%s\n' "$brakeman_version" | grep -Eq "(^|[^0-9.])${BRAKEMAN_VERSION}([^0-9.]|$)"; then | ||
| echo "::warning::Brakeman installed version mismatch - manifest will record missing output." | ||
| else | ||
| index=0 | ||
| while IFS= read -r -d '' root; do | ||
| root="${root#./}" | ||
| if [ -z "$root" ]; then root="."; fi | ||
| if [[ "$root" == ".." || "$root" == "../"* || "$root" == *"/.." || "$root" == *"/../"* ]]; then | ||
| echo "::warning::Skipping Brakeman root with traversal segments: ${root}" | ||
| continue | ||
| fi | ||
| if [ "$root" = "." ]; then | ||
| root_abs="$SOURCE_DIR" | ||
| elif ! root_abs="$(cd "$SOURCE_DIR/$root" && pwd -P)"; then | ||
| echo "::warning::Unable to resolve Brakeman root ${root} - skipping." | ||
| continue | ||
| fi | ||
| if [[ "$root_abs" != "$SOURCE_DIR" && "$root_abs" != "$SOURCE_DIR"/* ]]; then | ||
| echo "::warning::Brakeman root ${root} resolves outside source directory - skipping." | ||
| continue | ||
| fi | ||
| root_raw="$brakeman_raw_dir/brakeman-${index}.raw.sarif" | ||
| root_normalized="$brakeman_raw_dir/brakeman-${index}.sarif" | ||
| if brakeman \ | ||
| --path "$root_abs" \ | ||
| --config-file "$brakeman_config" \ | ||
| --ignore-config "$brakeman_ignore" \ | ||
| --show-ignored \ | ||
| --no-exit-on-warn \ | ||
| --no-exit-on-error \ | ||
| --format sarif \ | ||
| --output "$root_raw" \ | ||
| --quiet; then | ||
| : | ||
| else | ||
| echo "::warning::Brakeman scan for ${root} exited non-zero - using SARIF output if present." | ||
| fi | ||
| if [ -s "$root_raw" ]; then | ||
| if python3 "$RUNNER_DIR/.github/scripts/brakeman_sarif_paths.py" \ | ||
| "$root_raw" "$root_normalized" --root "$root" --base-dir "$SOURCE_DIR" \ | ||
| && [ -s "$root_normalized" ]; then | ||
| raw_files+=("$root_normalized") | ||
| else | ||
| echo "::warning::Brakeman SARIF path normalization failed for ${root} - discarding output." | ||
| fi | ||
| else | ||
| echo "::warning::Brakeman scan for ${root} produced no SARIF output." | ||
| fi | ||
| index=$((index + 1)) | ||
| done < "$roots_list" | ||
| if [ "${#raw_files[@]}" -eq 0 ]; then | ||
| echo "::warning::Brakeman produced no SARIF output for detected Rails roots - manifest will record missing output." | ||
| elif [ "${#raw_files[@]}" -eq 1 ]; then | ||
| cp "${raw_files[0]}" "$raw" \ | ||
| || { echo "::warning::Brakeman failed to copy SARIF output - manifest will record missing output."; rm -f "$raw"; } | ||
| else | ||
| python3 "$RUNNER_DIR/.github/scripts/sigilix_sarif_merge.py" \ | ||
| "${raw_files[@]}" -o "$raw" \ | ||
| || { echo "::warning::Brakeman SARIF merge failed - manifest will record missing output."; rm -f "$raw"; } | ||
| fi | ||
| fi | ||
| fi | ||
|
|
||
| if [ -s "$raw" ]; then | ||
| python3 "$RUNNER_DIR/.github/scripts/sigilix_sarif_contract.py" \ | ||
| brakeman "$raw" "$out" --cap "$RESULT_CAP" --ensure-run \ | ||
| || echo "::warning::Brakeman SARIF normalization failed - manifest will record missing output." | ||
| fi |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| : "${OPENGREP_MANYLINUX_AARCH64_SHA256:?}" | ||
| : "${OPENGREP_MANYLINUX_X86_SHA256:?}" | ||
| : "${OPENGREP_VERSION:?}" | ||
| : "${RESULT_CAP:?}" | ||
| : "${RUNNER_DIR:?}" | ||
| : "${RUNNER_TEMP:?}" | ||
| : "${SARIF_DIR:?}" | ||
| : "${SOURCE_DIR:?}" | ||
| : "${OPENGREP_CONFIG:=p/security-audit,p/owasp-top-ten}" | ||
|
|
||
| SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd -P)" | ||
| RUNNER_DIR="$(cd "$RUNNER_DIR" && pwd -P)" | ||
|
|
||
| raw="$SARIF_DIR/opengrep.raw.sarif" | ||
| out="$SARIF_DIR/opengrep.sarif" | ||
| opengrep_bin="$RUNNER_TEMP/opengrep" | ||
| asset="" | ||
| checksum="" | ||
| config_args=() | ||
|
|
||
| mkdir -p "$SARIF_DIR" "$RUNNER_TEMP" | ||
|
|
||
| cleanup_opengrep() { | ||
| rm -f "$opengrep_bin" | ||
| } | ||
| trap cleanup_opengrep EXIT | ||
|
|
||
| parse_opengrep_configs() { | ||
| local item | ||
| local trimmed | ||
| IFS=',' read -r -a config_items <<< "$OPENGREP_CONFIG" | ||
| for item in "${config_items[@]}"; do | ||
| trimmed="${item#"${item%%[![:space:]]*}"}" | ||
| trimmed="${trimmed%"${trimmed##*[![:space:]]}"}" | ||
| if [ -z "$trimmed" ]; then | ||
| echo "::warning::OpenGrep config contains an empty ruleset - manifest will record missing output." | ||
| return 1 | ||
| fi | ||
| if [[ "$trimmed" == -* ]]; then | ||
| echo "::warning::OpenGrep config '$trimmed' must not start with '-' - manifest will record missing output." | ||
| return 1 | ||
| fi | ||
| if [[ ! "$trimmed" =~ ^[A-Za-z0-9._/@-]+$ ]]; then | ||
| echo "::warning::OpenGrep config '$trimmed' contains unsupported characters - manifest will record missing output." | ||
| return 1 | ||
| fi | ||
| config_args+=(--config "$trimmed") | ||
| done | ||
| if [ "${#config_args[@]}" -eq 0 ]; then | ||
| echo "::warning::OpenGrep config is empty - manifest will record missing output." | ||
| return 1 | ||
| fi | ||
| } | ||
|
|
||
| case "$(uname -m)" in | ||
| x86_64|amd64) | ||
| asset="opengrep_manylinux_x86" | ||
| checksum="$OPENGREP_MANYLINUX_X86_SHA256" | ||
| ;; | ||
| aarch64|arm64) | ||
| asset="opengrep_manylinux_aarch64" | ||
| checksum="$OPENGREP_MANYLINUX_AARCH64_SHA256" | ||
| ;; | ||
| *) | ||
| echo "::warning::OpenGrep unsupported runner architecture $(uname -m) - manifest will record missing output." | ||
| ;; | ||
| esac | ||
|
|
||
| cd "$SOURCE_DIR" | ||
| if [ -z "$asset" ]; then | ||
| : | ||
| elif [[ ! "$OPENGREP_VERSION" =~ ^[0-9]+[.][0-9]+[.][0-9]+$ ]]; then | ||
| echo "::warning::OpenGrep version must be a pinned x.y.z version - manifest will record missing output." | ||
| elif [[ ! "$checksum" =~ ^[0-9a-f]{64}$ ]]; then | ||
| echo "::warning::OpenGrep checksum must be a pinned SHA256 value - manifest will record missing output." | ||
| elif ! parse_opengrep_configs; then | ||
| : | ||
| elif ! curl -fsSL -o "$opengrep_bin" \ | ||
| "https://github.com/opengrep/opengrep/releases/download/v${OPENGREP_VERSION}/${asset}"; then | ||
| echo "::warning::OpenGrep download failed - manifest will record missing output." | ||
| elif ! printf '%s %s\n' "$checksum" "$opengrep_bin" | sha256sum -c --strict -; then | ||
| echo "::warning::OpenGrep checksum mismatch - manifest will record missing output." | ||
| elif ! chmod +x "$opengrep_bin"; then | ||
| echo "::warning::OpenGrep chmod failed - manifest will record missing output." | ||
| elif ! opengrep_version="$("$opengrep_bin" --version 2>/dev/null)"; then | ||
| echo "::warning::OpenGrep version check failed - manifest will record missing output." | ||
| elif ! printf '%s\n' "$opengrep_version" | grep -Eq "(^|[^0-9.])${OPENGREP_VERSION}([^0-9.]|$)"; then | ||
| echo "::warning::OpenGrep installed version mismatch - manifest will record missing output." | ||
| else | ||
| "$opengrep_bin" scan \ | ||
| "${config_args[@]}" \ | ||
| --exclude=node_modules \ | ||
| --exclude=dist \ | ||
| --exclude=build \ | ||
| --exclude=coverage \ | ||
| --exclude=.next \ | ||
| --exclude=out \ | ||
| --exclude=.venv \ | ||
| --exclude=vendor \ | ||
| --exclude=.tox \ | ||
| --exclude=.terraform \ | ||
| --sarif-output="$raw" \ | ||
| . || true | ||
| if [ ! -s "$raw" ]; then | ||
| echo "::warning::OpenGrep scan produced no SARIF output - manifest will record missing output." | ||
| fi | ||
| fi | ||
|
|
||
| if [ -s "$raw" ]; then | ||
| python3 "$RUNNER_DIR/.github/scripts/sigilix_sarif_contract.py" \ | ||
| opengrep "$raw" "$out" --cap "$RESULT_CAP" --ensure-run \ | ||
| || echo "::warning::OpenGrep SARIF normalization failed - manifest will record missing output." | ||
| fi | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: Sigilix/runner
Length of output: 85
Reject trailing empty OpenGrep rulesets too
IFS=',' read -r -a config_items <<< "$OPENGREP_CONFIG"drops a terminal empty field: withOPENGREP_CONFIG='p/security-audit,'the parser sees only one element (<p/security-audit>) and never hits the-z "$trimmed"rejection. This bypasses the “empty ruleset” validation contract for trailing-comma input.🤖 Prompt for AI Agents