#!/bin/bash

## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

## AI-Assisted

## Dump open code-scanning alerts across the active orgs as a markdown
## table on stdout. Status messages go to stderr (via log) so the
## report can be cleanly redirected:
##
##   dm-github-org-security-report --report > /tmp/sec.md
##
## --report filters to "code-fixable" tools (CodeQL, Bandit, cppcheck,
## Coverity) that produce file+line findings the operator can edit.
## --all shows every open alert including Scorecard's policy-style
## findings (see docs/scorecard-known-false-positives.md).
##
## See agents/github-actions.md for the per-tool trust boundary.

set -o errexit
set -o nounset
set -o pipefail
set -o errtrace
shopt -s inherit_errexit
shopt -s shift_verbose

# shellcheck source=../libexec/developer-meta-files/github-org-lib.bsh
source /usr/libexec/developer-meta-files/github-org-lib.bsh
# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/log_run_die.sh
source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/log_run_die.sh

## Symmetry with dm-github-org-policy's ORGS_OVERRIDE convention so
## the same wrapper runs in tests.
readonly SOURCE_ORGS=( 'Kicksecure' 'Whonix' )
readonly MIRROR_ORGS=( 'org-ai-assisted' )
if [ -n "${ORGS_OVERRIDE:-}" ]; then
   IFS=',' read -ra ORGS <<< "${ORGS_OVERRIDE}"
else
   ORGS=( "${MIRROR_ORGS[@]}" )
fi
readonly ORGS

## "Code-fixable" tools - produce file+line findings the operator can
## edit. Used as a jq regex in the --report filter; single source of
## truth for the tool list.
readonly CODE_TOOLS_RE='^(CodeQL|Bandit|cppcheck|Coverity)$'

mode=''
mode_set=0

show_help() {
   cat <<'EOF'
Dump open code-scanning alerts as a markdown table on stdout.

Usage (one mode flag is required):
  dm-github-org-security-report --report   code-fixable tools only
  dm-github-org-security-report --all      every open alert
  dm-github-org-security-report --help

Default --report filter: tool name in (CodeQL, Bandit, cppcheck,
Coverity). Drops Scorecard alerts; those are project-policy
shaped (see docs/scorecard-known-false-positives.md) and rarely
"edit a line of code to fix".

Output goes to stdout (markdown). Status / fetch progress goes
to stderr via log.

Auth:
  ${GITHUB_TOKEN} env var, or chmod-600 ~/.config/github-token.
  Token needs 'security_events' (classic) or 'Code scanning
  alerts: read' (fine-grained).
EOF
}

while [ "$#" -gt 0 ]; do
   case "$1" in
      --report)
         [ "${mode_set}" -eq 0 ] \
            || die 64 "conflicting mode flags: '${mode}' vs '$1'"
         mode='report'
         mode_set=1
         shift
         ;;
      --all)
         [ "${mode_set}" -eq 0 ] \
            || die 64 "conflicting mode flags: '${mode}' vs '$1'"
         mode='all'
         mode_set=1
         shift
         ;;
      -h|--help)
         show_help
         exit 0
         ;;
      *)
         die 64 "unknown arg: '$1'"
         ;;
   esac
done

[ "${mode_set}" -eq 1 ] \
   || die 64 "mode flag required: --report or --all"

policy_warn_seen=0

case "${mode}" in
   report)
      filter_jq='.[] | select(.tool.name | test("'"${CODE_TOOLS_RE}"'"))'
      ;;
   all)
      filter_jq='.[]'
      ;;
esac

## Operator-facing status goes to stderr via log; this is one of the
## cases R-041 explicitly carves out for bare printf (writing to a
## pipe-shaped consumer).
## R-033: hoist the timestamp; do not inline $(date ...) into the
## printf format string.
report_timestamp="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
## Markdown structural blank line between the H1 and the table is
## required by the renderer; R-031 'embed newlines in one string'
## keeps the report markdown valid without tripping R-042.
printf '%s\n' "# Security report - ${report_timestamp} - mode=${mode}

| repo | tool | severity | rule | path | line | message |
| --- | --- | --- | --- | --- | --- | --- |"

total_rows=0
for org in "${ORGS[@]}"; do
   log notice "fetching alerts for '${org}'..."
   ## /code-scanning/alerts paginates at per_page<=100. One page is
   ## enough at current scale (~100 total org-wide); add a Link-header
   ## walk if alert count outgrows it.
   result="$(ghorg_api GET "/orgs/${org}/code-scanning/alerts?state=open&per_page=100")" \
      || { log warn "${org}: api call failed"; policy_warn_seen=1; continue; }
   status="$(ghorg_status_of "${result}")"
   if [ "${status}" != '200' ]; then
      log warn "${org}: HTTP ${status}"
      policy_warn_seen=1
      continue
   fi
   body="$(ghorg_body_of "${result}")"
   ## Escape "|" in message text so it does not break the table
   ## column separator.
   rows="$(printf '%s' "${body}" | ghorg_jq_capped -r -- "
      ${filter_jq}
      | [
            .repository.full_name,
            .tool.name,
            (.rule.severity // \"-\"),
            (.rule.id // \"-\"),
            (.most_recent_instance.location.path // \"-\"),
            ((.most_recent_instance.location.start_line // 0) | tostring),
            ((.most_recent_instance.message.text // \"-\")
               | gsub(\"\\\\|\"; \"\\\\\\\\|\")
               | gsub(\"\\n\"; \" \"))
         ]
      | \"| \" + join(\" | \") + \" |\"
   ")" || { log warn "${org}: jq parse failed"; policy_warn_seen=1; continue; }

   if [ -z "${rows}" ]; then
      log notice "  ${org}: no matching alerts"
      continue
   fi
   while IFS= read -r row; do
      [ -z "${row}" ] && continue
      printf '%s\n' "${row}"
      total_rows=$(( total_rows + 1 ))
   done <<< "${rows}"
done

log notice "total rows in report: ${total_rows}"
[ "${policy_warn_seen}" -eq 1 ] && exit 1
exit 0
