From 8f25afb89b5bce10b4e3e3e7172feb5207436c3d Mon Sep 17 00:00:00 2001 From: Rich Joiner Date: Fri, 5 Jun 2026 16:03:06 +0000 Subject: [PATCH 1/4] NV-4418 Fix and harden the GitLab Easy Mode demo script Make gitlab-demo.sh run clean on first try and safe to re-run. - Fix the showstopper token line: it read a nonexistent 'token' file and aborted under set -e before anything was created. Pass the captured $TOKEN as the value argument and drop the '< token' redirect, and guard against an empty/whitespace token before setting the masked CI variable. - Create the repo in the correct namespace ("$GROUP/$REPO"). - Quote all expansions; add set -u -o pipefail. - Idempotency: skip the clone if present; guard repo/app/target creation and the masked CI variable so a second run reuses existing resources with a logged warning instead of erroring out; reconcile the gitlab remote to the current "$GROUP/$REPO" so a re-run with different arguments does not push to the previous run's project. - Pass '--type API' to target create, matching the casing the NightVision docs use. - Hygiene: print a notice and document a revert command for the /etc/hosts edit; label the echoed credentials as the javaspringvulny demo defaults; use literal placeholders in the cleanup notes, since script-internal variables do not expand in a fresh shell. Phase 1 GitLab consolidation is tracked in NV-4412. --- demo-scripts/gitlab-demo.sh | 116 ++++++++++++++++++++++++++---------- 1 file changed, 85 insertions(+), 31 deletions(-) diff --git a/demo-scripts/gitlab-demo.sh b/demo-scripts/gitlab-demo.sh index ee7c191..ff94153 100755 --- a/demo-scripts/gitlab-demo.sh +++ b/demo-scripts/gitlab-demo.sh @@ -1,5 +1,21 @@ #!/usr/bin/env bash -set -e +set -euo pipefail + +# --------------------------------------------------------------------------------------------------------------------- +# NightVision GitLab "Easy Mode" demo (NV-4418) +# +# Creates a GitLab project, wires the NIGHTVISION_TOKEN CI variable, creates a NightVision +# app + target, records auth, and pushes the demo repo to trigger the CI/CD pipeline. +# +# Re-run behaviour: this script is intended to be safe to re-run. The create steps +# (GitLab repo, NightVision app/target) are guarded so a second run reuses existing +# resources with a logged warning instead of aborting, and the gitlab remote is +# reconciled to the current "$GROUP/$REPO" on each run. The NightVision token is +# the exception: each run creates a fresh one and overwrites the CI variable (the value +# cannot be read back, so it is rotated, not reused); older tokens stay in NightVision, so +# revoke them there if you re-run often. The script does NOT delete anything; see the +# cleanup notes at the bottom to tear a demo down. +# --------------------------------------------------------------------------------------------------------------------- # Check if the correct number of arguments is provided if [ "$#" -ne 2 ]; then @@ -9,70 +25,108 @@ if [ "$#" -ne 2 ]; then fi # Assign positional arguments to variables -GROUP=$1 -REPO=$2 +GROUP="$1" +REPO="$2" echo "Creating a repository under: $GROUP/$REPO" -# Clone the GitHub repository that we will mirror to GitLab -git clone https://github.com/nvsecurity/java-github-actions-demo +# Clone the GitHub repository that we will mirror to GitLab. +# Idempotent: reuse an existing checkout instead of failing on a second run. +if [ ! -d java-github-actions-demo ]; then + git clone https://github.com/nvsecurity/java-github-actions-demo +fi cd java-github-actions-demo # --------------------------------------------------------------------------------------------------------------------- # Set up the GitLab repository # --------------------------------------------------------------------------------------------------------------------- -# Create a repository -echo "NOTE: Select NO for 'Create a local project directory'" -glab repo create $REPO - -# add the nightvision token as the GitLab secret NIGHTVISION_TOKEN -nightvision login -TOKEN=$(nightvision token create) - -# Check if GitLab CLI is authenticated +# Authenticate to GitLab and NightVision first, so the create steps below only run after +# both logins are confirmed - an aborted login then leaves nothing half-created. if ! glab auth status 2>&1 | grep -q 'Logged in to gitlab.com'; then echo "GitLab CLI not authenticated. Running 'glab auth login'..." glab auth login else echo "Logged in to gitlab already..." fi +nightvision login -glab variable set NIGHTVISION_TOKEN --masked --repo $GROUP/$REPO $TOKEN < token +# Create a repository in the target namespace. If it already exists, keep going. +echo "NOTE: Select NO for 'Create a local project directory'" +glab repo create "$GROUP/$REPO" || echo "WARNING: 'glab repo create $GROUP/$REPO' failed (the project may already exist); continuing." + +# Create a fresh NightVision token for the NIGHTVISION_TOKEN CI variable. tr strips any +# stray whitespace so the value GitLab masks is clean; the guard catches an empty result +# (an exit-0 token create with no output) before it becomes an invalid masked variable. +TOKEN="$(nightvision token create | tr -d '[:space:]')" +if [ -z "$TOKEN" ]; then + echo "ERROR: 'nightvision token create' returned an empty token; aborting." >&2 + exit 1 +fi + +# Set the masked CI variable to the freshly created token (rotated every run, not reused, +# because a token's value cannot be read back). If it already exists (older glab errors +# instead of upserting), update it. This must succeed: failing both set and update aborts +# the script (set -e) rather than silently leaving CI a stale token. +glab variable set NIGHTVISION_TOKEN --masked --repo "$GROUP/$REPO" "$TOKEN" \ + || glab variable update NIGHTVISION_TOKEN --masked --repo "$GROUP/$REPO" "$TOKEN" # --------------------------------------------------------------------------------------------------------------------- # Note that GitLab has additional requirements vs other CI/CD providers. # Instead of `localhost` you must use the `docker` hostname. -# First add the docker hostname reference to your `/etc/hosts` file on your laptop +# First add the docker hostname reference to your `/etc/hosts` file on your laptop. +# +# This edits a system file with sudo. It is left in place after the demo so repeat runs work. +# To revert it afterwards run: +# sudo sed -i.bak '/^127\.0\.0\.1 docker$/d' /etc/hosts # --------------------------------------------------------------------------------------------------------------------- -if ! grep -q "127.0.0.1 docker" /etc/hosts; then \ - echo "127.0.0.1 docker" | sudo tee -a /etc/hosts; \ +if ! grep -q "127.0.0.1 docker" /etc/hosts; then + echo "NOTICE: adding '127.0.0.1 docker' to /etc/hosts (sudo). See the revert command in this script's comments." + echo "127.0.0.1 docker" | sudo tee -a /etc/hosts fi + # --------------------------------------------------------------------------------------------------------------------- # NightVision commands # --------------------------------------------------------------------------------------------------------------------- -# Create app and target -# Username: user -# Password: password +# Create app and target. Guarded so a re-run reuses the existing app/target. URL="https://docker:9000" APP="javaspringvulny-api-gitlab" -nightvision app create $APP -nightvision target create $APP https://docker:9000 --type api +nightvision app create "$APP" || echo "WARNING: 'nightvision app create $APP' failed (it may already exist); continuing." +nightvision target create "$APP" "$URL" --type API || echo "WARNING: 'nightvision target create $APP' failed (it may already exist); continuing." # Start the application docker compose up -d; sleep 10 -# Record authentication - click on Form Auth -echo "Click on Form Auth and use these credentials: " -echo "\tUsername: user" -echo "\tPassword: password" -nightvision auth playwright create $APP $URL +# Record authentication - click on Form Auth. +# These are the demo application's default credentials (the javaspringvulny sample app), +# not real secrets. +echo "Click on Form Auth and use the javaspringvulny demo defaults:" +echo " Username: user" +echo " Password: password" +nightvision auth playwright create "$APP" "$URL" # --------------------------------------------------------------------------------------------------------------------- # sync it back with GitLab and trigger the CI/CD job. # --------------------------------------------------------------------------------------------------------------------- -git remote add gitlab git@gitlab.com:$GROUP/$REPO.git +# Point the gitlab remote at the requested project. A reused checkout may carry the +# remote from a previous run with different arguments; reconciling it to the current +# "$GROUP/$REPO" keeps the push from silently targeting the previous run's project. +if git remote get-url gitlab >/dev/null 2>&1; then + git remote set-url gitlab "git@gitlab.com:$GROUP/$REPO.git" +else + git remote add gitlab "git@gitlab.com:$GROUP/$REPO.git" +fi +# Add an empty commit so each run pushes a fresh commit and triggers the +# pipeline. A re-run reuses the existing checkout, which has no new commits, so a +# bare push would be "Everything up-to-date" and fire nothing. +git commit --allow-empty -m "Trigger GitLab pipeline" git push gitlab main # Notes: -# To delete the project: -# glab repo delete $GROUP/$REPO +# To delete the project and local checkout (substitute the group and repo you ran this script with): +# glab repo delete / # rm -rf ./java-github-actions-demo +# The NightVision app and target are not deleted; they are reused on re-run +# (auth is re-recorded each run, and each run mints a fresh token that persists, +# as noted in the header). Remove the app, target, and stale tokens from the +# NightVision UI for a full teardown. +# To revert the /etc/hosts entry: +# sudo sed -i.bak '/^127\.0\.0\.1 docker$/d' /etc/hosts From 25f14904a854bbde68dd4eb4fbaeb6f908405180 Mon Sep 17 00:00:00 2001 From: Rich Joiner Date: Thu, 11 Jun 2026 12:44:01 +0000 Subject: [PATCH 2/4] NV-4418 Harden the GitHub Actions demo script Port the gitlab-demo.sh fixes to github-actions-demo.sh for consistency, and fix its own showstopper: the clone URL was built from 'git config user.name', a display name that is not a valid GitHub owner for most users. Derive the owner from the authenticated gh session instead. - Authenticate to GitHub and NightVision before any other step. - Capture the token in a variable instead of writing it to a 'token' file on disk; strip whitespace and abort on an empty result. - Pin 'gh secret set' to the fork with an explicit --repo instead of inferring the repository from the checkout's git remote. - Quote all expansions; add set -u -o pipefail. - Idempotency: reuse an existing clone only after confirming its origin points at the expected fork; guard app/target creation so a re-run reuses existing resources ('gh secret set' already upserts). - Hygiene: label the echoed credentials as the javaspringvulny demo defaults; add cleanup notes with literal placeholders. --- demo-scripts/github-actions-demo.sh | 100 ++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 27 deletions(-) diff --git a/demo-scripts/github-actions-demo.sh b/demo-scripts/github-actions-demo.sh index 35b77bb..713b691 100755 --- a/demo-scripts/github-actions-demo.sh +++ b/demo-scripts/github-actions-demo.sh @@ -1,42 +1,88 @@ -#!/bin/bash -set -e +#!/usr/bin/env bash +set -euo pipefail -# Clone the repository locally from your terminal -git clone https://github.com/$(git config user.name)/java-github-actions-demo.git -cd java-github-actions-demo - -# add the nightvision token as the github action secret NIGHTVISION_TOKEN -nightvision login -nightvision token create > token +# --------------------------------------------------------------------------------------------------------------------- +# NightVision GitHub Actions demo +# +# Clones your fork of java-github-actions-demo, wires the NIGHTVISION_TOKEN Actions secret, +# creates a NightVision app + target, records auth, and pushes a commit to trigger the +# workflow. +# +# Re-run behaviour: this script is intended to be safe to re-run. The create steps +# (NightVision app/target) are guarded so a second run reuses existing resources with a +# logged warning instead of aborting. The NightVision token is the exception: each run +# creates a fresh one and overwrites the Actions secret (the value cannot be read back, so +# it is rotated, not reused); older tokens stay in NightVision, so revoke them there if you +# re-run often. The script does NOT delete anything; see the cleanup notes at the bottom. +# --------------------------------------------------------------------------------------------------------------------- -# Check if GitHub CLI is authenticated +# Authenticate to GitHub and NightVision first, so the steps below only run after both +# logins are confirmed - an aborted login then leaves nothing half-created. if ! gh auth status 2>&1 | grep -q 'Logged in to github.com'; then echo "GitHub CLI not authenticated. Running 'gh auth login'..." gh auth login else echo "Logged in to github already..." fi +nightvision login + +# Clone your fork of the demo repository. The owner comes from the authenticated GitHub +# login: 'git config user.name' is a display name, not a valid URL segment for most users. +# Idempotent: reuse an existing checkout instead of failing on a second run, but only +# after confirming it points at the expected fork - a stale checkout of another repo +# would otherwise receive the push at the end of the script. +OWNER="$(gh api user --jq .login)" +if [ ! -d java-github-actions-demo ]; then + git clone "https://github.com/$OWNER/java-github-actions-demo.git" +elif ! git -C java-github-actions-demo remote get-url origin | grep -qiE "github\.com[:/]$OWNER/java-github-actions-demo(\.git)?$"; then + echo "ERROR: existing ./java-github-actions-demo does not point at $OWNER/java-github-actions-demo; move it aside and re-run." >&2 + exit 1 +fi +cd java-github-actions-demo + +# Create a fresh NightVision token for the NIGHTVISION_TOKEN Actions secret, without +# writing it to disk. tr strips any stray whitespace; the guard catches an empty result +# (an exit-0 token create with no output) before it becomes an empty secret. +TOKEN="$(nightvision token create | tr -d '[:space:]')" +if [ -z "$TOKEN" ]; then + echo "ERROR: 'nightvision token create' returned an empty token; aborting." >&2 + exit 1 +fi -# Use GitHub CLI to set NIGHTVISION_TOKEN -gh secret set NIGHTVISION_TOKEN < token -rm token +# Set the Actions secret to the freshly created token (rotated every run, not reused, +# because a token's value cannot be read back). 'gh secret set' upserts, so no +# already-exists guard is needed; a real failure aborts the script (set -e). The explicit +# --repo pins the secret to your fork rather than inferring it from the checkout's remote. +gh secret set NIGHTVISION_TOKEN --body "$TOKEN" --repo "$OWNER/java-github-actions-demo" -# Create app and target -# Username: user -# Password: password +# --------------------------------------------------------------------------------------------------------------------- +# NightVision commands +# --------------------------------------------------------------------------------------------------------------------- +# Create app and target. Guarded so a re-run reuses the existing app/target. URL="https://localhost:9000" APP="javaspringvulny-api" -nightvision app create $APP -nightvision target create $APP $URL --type API +nightvision app create "$APP" || echo "WARNING: 'nightvision app create $APP' failed (it may already exist); continuing." +nightvision target create "$APP" "$URL" --type API || echo "WARNING: 'nightvision target create $APP' failed (it may already exist); continuing." + # Start the application docker compose up -d; sleep 10 -# Record authentication - click on Form Auth -echo "Click on Form Auth and use these credentials: " -echo "\tUsername: user" -echo "\tPassword: password" -nightvision auth playwright create $APP $URL - -# Add a commit and trigger the CI/CD -echo "foobar" >> README.md -git commit -am 'trigger a github action' +# Record authentication - click on Form Auth. +# These are the demo application's default credentials (the javaspringvulny sample app), +# not real secrets. +echo "Click on Form Auth and use the javaspringvulny demo defaults:" +echo " Username: user" +echo " Password: password" +nightvision auth playwright create "$APP" "$URL" + +# --------------------------------------------------------------------------------------------------------------------- +# Add an empty commit and push to trigger the GitHub Actions workflow. An empty +# commit always provides a fresh commit to push (including on a re-run against an +# existing checkout) without modifying tracked files. +# --------------------------------------------------------------------------------------------------------------------- +git commit --allow-empty -m "Trigger GitHub Actions workflow" git push + +# Notes: +# To clean up (substitute your GitHub login): +# gh secret delete NIGHTVISION_TOKEN --repo /java-github-actions-demo +# rm -rf ./java-github-actions-demo From 87d3f4ae70d3eab6331557657fb2c8ff55e8f6c1 Mon Sep 17 00:00:00 2001 From: Rich Joiner Date: Thu, 11 Jun 2026 18:10:05 +0000 Subject: [PATCH 3/4] NV-4418 Add shell lint gate with shared local/CI entrypoint Add a static lint gate for the repo's shell scripts so the hardening on the demo scripts is enforced going forward instead of relying on manual checks. - tests/run.sh is the single entrypoint: it runs bash -n and shellcheck over every git-tracked *.sh file. Both local dev and CI call this exact script, so the two can never check different things or drift apart. - The script enumerates targets via git ls-files so untracked / vendored files are never linted, and treats a missing shellcheck as a hard error rather than silently downgrading to a weaker check than CI. - .github/workflows/lint.yml runs tests/run.sh on pushes to main and on all pull requests, with a read-only permissions default and a SHA-pinned actions/checkout. - .github/dependabot.yml keeps that SHA pin refreshed as reviewable PRs, with a 7-day cooldown so freshly published versions are not pinned immediately. This matches the GitHub Actions security posture used in the demo target repo. --- .github/dependabot.yml | 11 ++++++++ .github/workflows/lint.yml | 28 ++++++++++++++++++++ tests/run.sh | 52 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/lint.yml create mode 100755 tests/run.sh diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b0239f6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + # Wait 7 days after a release is published before opening an update PR, + # so freshly published (potentially compromised) versions are not pinned + # immediately. Keeps the SHA pins refreshed as reviewable PRs. + cooldown: + default-days: 7 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..ca321df --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,28 @@ +name: Lint + +# Static checks for the repo's shell scripts. Runs the same tests/run.sh that +# developers run locally, so local and CI can never check different things. +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + shell-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Ensure shellcheck is available + run: | + if ! command -v shellcheck >/dev/null 2>&1; then + sudo apt-get update && sudo apt-get install -y shellcheck + fi + shellcheck --version + + - name: Run lint entrypoint + run: ./tests/run.sh diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 0000000..235a592 --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Shared lint entrypoint for nv-public-reference. CI (.github/workflows/lint.yml) +# invokes this exact script, so a local run and a CI run can never check different +# things or drift apart over time. +# +# What it checks, for every tracked shell script: +# 1. bash -n - syntax / parse check +# 2. shellcheck - static analysis (quoting, set -e pitfalls, unsafe expansions) +# +# Usage: tests/run.sh +set -euo pipefail + +# Run from the repository root regardless of the caller's working directory, so the +# git file list and relative paths resolve identically in local and CI runs. +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo_root" + +# The shellcheck tool is required, not optional: silently skipping it when absent +# would let a local run pass on a weaker check than CI, which is the exact drift +# this shared entrypoint exists to prevent. +if ! command -v shellcheck >/dev/null 2>&1; then + echo "ERROR: shellcheck not found on PATH. Install it and re-run:" >&2 + echo " macOS: brew install shellcheck" >&2 + echo " Debian: sudo apt-get install -y shellcheck" >&2 + echo " other: https://github.com/koalaman/shellcheck#installing" >&2 + exit 1 +fi + +# Enumerate tracked shell scripts via git so untracked / vendored files (for example +# the .agent-sandbox-config tree) are never linted. A read loop (rather than mapfile) +# keeps this working on bash 3.2, the default on macOS, so local runs match CI. +scripts=() +while IFS= read -r script_path; do + scripts+=("$script_path") +done < <(git ls-files '*.sh') +if [ "${#scripts[@]}" -eq 0 ]; then + echo "No tracked *.sh files found; nothing to lint." + exit 0 +fi + +echo "Linting ${#scripts[@]} shell script(s):" +printf ' %s\n' "${scripts[@]}" + +# Cheap parse check first; set -e aborts on the first failure so CI fails the job. +for script in "${scripts[@]}"; do + bash -n "$script" +done + +# Then the deeper static analysis pass over the whole set in one invocation. +shellcheck "${scripts[@]}" + +echo "OK: all shell scripts passed bash -n and shellcheck." From 7e7d040492fe213666fc2bfaadab9085d00d9fa9 Mon Sep 17 00:00:00 2001 From: Rich Joiner Date: Thu, 11 Jun 2026 18:14:12 +0000 Subject: [PATCH 4/4] NV-4418 Add .gitignore for build, sandbox, and local artifacts The repo had no .gitignore, leaving Python bytecode and local-only trees exposed to accidental commits. - Ignore __pycache__ / *.pyc from the sarif/ converters. - Ignore the java-github-actions-demo checkout the demo scripts clone into, along with the scan artifacts written inside it. - Ignore the local .agent-sandbox-config tree and the analysis/ work-product directory, neither of which is part of the published reference material. --- .gitignore | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8fdc7b --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Python bytecode / caches (sarif/ converters) +__pycache__/ +*.py[cod] + +# OS metadata +.DS_Store +Thumbs.db + +# Editor / IDE +.idea/ +.vscode/ +*.swp + +# Local checkout the demo scripts clone into, plus any scan artifacts +# (openapi-spec.yml, scan-results.txt, results.sarif) written inside it. +/java-github-actions-demo/ + +# Agent sandbox state: local tooling, never part of the repo. +.agent-sandbox-config/ + +# Local analysis / work products, kept out of version control by convention. +analysis/