diff --git a/.github/workflows/code-scanning.yml b/.github/workflows/code-scanning.yml new file mode 100644 index 0000000..0fdb615 --- /dev/null +++ b/.github/workflows/code-scanning.yml @@ -0,0 +1,70 @@ +name: Code scanning + +on: + workflow_dispatch: + workflow_run: + workflows: [Release] + types: [completed] + pull_request: + types: + - opened + - synchronize + branches: [main] + paths: + - "lib/*.nix" + - "pkgs/*.nix" + - "scripts/trivy-scan/**/*" + +jobs: + trivy-scan: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event.pull_request.id }} + outputs: + sarif_files: ${{ steps.sarif_files.outputs.sarif_files }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install Nix + uses: cachix/install-nix-action@v31 + - name: Build SARIF file + run: nix run '.#trivy-scan' + - name: Get SARIF files + id: sarif_files + run: | + sarifs=(.trivy/scan_results/*.sarif) + json_array=$(printf '%s\n' "${sarifs[@]}" | jq -R . | jq -sc .) + echo "sarif_files=$json_array" >> $GITHUB_OUTPUT + - name: Upload SARIF files + uses: actions/upload-artifact@v4 + with: + name: sarif_files + include-hidden-files: true + path: .trivy/scan_results/*.sarif + + upload-sarifs: + needs: trivy-scan + runs-on: ubuntu-latest + permissions: + security-events: write + strategy: + fail-fast: false + matrix: + sarif_file: ${{ fromJson(needs.trivy-scan.outputs.sarif_files) }} + steps: + - name: Download SARIF files + uses: actions/download-artifact@v4 + with: + name: sarif_files + path: .trivy/scan_results + - name: Extract category from filename + id: category + run: | + filename=$(basename "${{ matrix.sarif_file }}") + category="${filename%.sarif}" + echo "category=$category" >> $GITHUB_OUTPUT + - name: "Upload sarif file: ${{ matrix.sarif_file }}" + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: ${{ matrix.sarif_file }} + category: ${{ steps.category.outputs.category }} diff --git a/.github/workflows/housekeeping.yml b/.github/workflows/housekeeping.yml index c5e9464..6ef5edc 100644 --- a/.github/workflows/housekeeping.yml +++ b/.github/workflows/housekeeping.yml @@ -1,7 +1,6 @@ name: Cleanup on: - repository_dispatch: workflow_dispatch: schedule: # Run every day diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8282e4..2c626bb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,12 +2,12 @@ name: Release on: push: - branches: - - main + branches: [main] paths: - .github/workflows/release.yml - "**.nix" - flake.lock + - "!scripts/**/*" env: REGISTRY: ghcr.io @@ -38,7 +38,7 @@ jobs: - id: get-images run: | nix flake show --json --quiet --quiet \ - | jq -r '.packages["x86_64-linux"] | delpaths([["default"],["geolite2"]]) | keys | "images=" + (. | tostring)' \ + | jq -r '.packages["x86_64-linux"] | delpaths([["default"],["geolite2"],["trivy-scan"]]) | keys | "images=" + (. | tostring)' \ >> $GITHUB_OUTPUT build-and-push: diff --git a/.github/workflows/update-geolite.yml b/.github/workflows/update-geolite.yml index db4801f..85a8ccc 100644 --- a/.github/workflows/update-geolite.yml +++ b/.github/workflows/update-geolite.yml @@ -1,7 +1,6 @@ name: Update GeoLite on: - repository_dispatch: workflow_dispatch: schedule: # Run every day @@ -34,8 +33,8 @@ jobs: uses: peter-evans/create-pull-request@v7 with: token: ${{ secrets.GITHUB_TOKEN }} - commit-message: "refactor(pkgs/geolite2): update database" - title: "refactor(pkgs/geolite2): update database" + commit-message: "refactor(pkgs): update geolite2" + title: "refactor(pkgs): update geolite2" body: | Automated update of GeoLite2 database. diff --git a/.gitignore b/.gitignore index 2efaad0..088b12c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .direnv result* +.trivy diff --git a/default.nix b/default.nix index 6e05844..1da6571 100644 --- a/default.nix +++ b/default.nix @@ -23,8 +23,8 @@ let xlib = import ./lib { inherit nix2container pkgs xpkgs; }; in -xlib.mkAllImages +pkgs.lib.mapAttrs (_: value: value.image) xlib.mkAllImages // xpkgs // { - default = xlib.mkImage { }; + default = (xlib.mkImage { }).image; } diff --git a/flake.lock b/flake.lock index 9a1151d..bd0c20b 100644 --- a/flake.lock +++ b/flake.lock @@ -1,6 +1,6 @@ { "nodes": { - "nix2container": { + "n2c": { "inputs": { "nixpkgs": [ "nixpkgs" @@ -38,7 +38,7 @@ }, "root": { "inputs": { - "nix2container": "nix2container", + "n2c": "n2c", "nixpkgs": "nixpkgs", "treefmt-nix": "treefmt-nix" } diff --git a/flake.nix b/flake.nix index d5303a4..ba2614d 100644 --- a/flake.nix +++ b/flake.nix @@ -4,7 +4,7 @@ inputs = { nixpkgs.url = "github:nixos/nixpkgs/25.05"; - nix2container = { + n2c = { url = "github:nlewo/nix2container"; inputs.nixpkgs.follows = "nixpkgs"; }; @@ -19,7 +19,7 @@ { self, nixpkgs, - nix2container, + n2c, treefmt-nix, ... }: @@ -34,8 +34,8 @@ f: nixpkgs.lib.genAttrs systems ( system: - f ( - import nixpkgs { + f rec { + pkgs = import nixpkgs { inherit system; config.allowUnfreePredicate = @@ -43,38 +43,60 @@ builtins.elem (nixpkgs.lib.getName pkg) [ "geolite2" ]; - } - ) + }; + + xpkgs = pkgs.lib.packagesFromDirectoryRecursive { + inherit (pkgs) callPackage; + + directory = ./pkgs; + }; + + nix2container = n2c.packages.${pkgs.system}; + } ); - treefmtEval = eachSystem (pkgs: treefmt-nix.lib.evalModule pkgs ./treefmt.nix); + treefmtEval = eachSystem ({ pkgs, ... }: treefmt-nix.lib.evalModule pkgs ./treefmt.nix); in { # nix flake check - checks = eachSystem (pkgs: { - formatting = treefmtEval.${pkgs.system}.config.build.check self; - }); + checks = eachSystem ( + { pkgs, ... }: + { + formatting = treefmtEval.${pkgs.system}.config.build.check self; + } + ); # nix fmt - formatter = eachSystem (pkgs: treefmtEval.${pkgs.system}.config.build.wrapper); + formatter = eachSystem ({ pkgs, ... }: treefmtEval.${pkgs.system}.config.build.wrapper); # Development environment with tools available in PATH - devShells = eachSystem (pkgs: { - default = pkgs.callPackage ./shell.nix { }; - }); + devShells = eachSystem ( + { + pkgs, + xpkgs, + nix2container, + ... + }: + { + default = pkgs.callPackage ./shell.nix { + inherit xpkgs nix2container; + }; + } + ); packages = eachSystem ( - pkgs: - let - xpkgs = pkgs.lib.packagesFromDirectoryRecursive { - inherit (pkgs) callPackage; - - directory = ./pkgs; - }; - in - import ./default.nix { + { + pkgs, + xpkgs, + nix2container, + ... + }: + (import ./default.nix { inherit pkgs xpkgs; - inherit (nix2container.packages.${pkgs.system}) nix2container; - } + inherit (nix2container) nix2container; + }) + // (import ./scripts { + inherit pkgs xpkgs nix2container; + }) ); }; } diff --git a/lib/default.nix b/lib/default.nix index 5fd127b..8a3ecd0 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -87,10 +87,6 @@ rec { } ); - in - nix2container.buildImage { - name = "goaccess"; - # Generate tag based on version and enabled features tag = lib.concatStrings [ goaccess.version @@ -98,33 +94,40 @@ rec { (lib.optionalString (!withGeolite2 && withGeolocation) "-geoip") (lib.optionalString (baseImage != "") "-${baseImage}") ]; + in + { + inherit tag; - # Set the base image to build from (empty string means scratch/no base) - fromImage = - if (baseImage != "") then + image = nix2container.buildImage { + inherit tag; + + name = "goaccess"; + + # Set the base image to build from + fromImage = lib.optionalString (baseImage != "") ( nix2container.pullImage (distros.${baseImage} or throwDistro) - else - baseImage; - - # Build the root filesystem environment - copyToRoot = buildEnv { - name = "root"; - - # Packages to include in the container - paths = - [ goaccessBuild ] - # Include GeoLite2 database if requested - ++ lib.optional withGeolite2 geolite2; - - # Directories to symlink into the container root - pathsToLink = [ - "/bin" - "/etc" - "/share" - ]; + ); + + # Build the root filesystem environment + copyToRoot = buildEnv { + name = "root"; + + # Packages to include in the container + paths = + [ goaccessBuild ] + # Include GeoLite2 database if requested + ++ lib.optional withGeolite2 geolite2; + + # Directories to symlink into the container root + pathsToLink = [ + "/bin" + "/etc" + "/share" + ]; + }; + + # Default command to run when container starts + config.Entrypoint = [ "goaccess" ]; }; - - # Default command to run when container starts - config.Entrypoint = [ "goaccess" ]; }; } diff --git a/scripts/_template/default.nix b/scripts/_template/default.nix new file mode 100644 index 0000000..f9a88cf --- /dev/null +++ b/scripts/_template/default.nix @@ -0,0 +1,14 @@ +{ writeShellApplication, coreutils, ... }: + +let + name = builtins.baseNameOf (builtins.toString ./.); +in +writeShellApplication { + inherit name; + + text = builtins.readFile ./script.sh; + + runtimeInputs = [ + coreutils + ]; +} diff --git a/scripts/_template/script.sh b/scripts/_template/script.sh new file mode 100755 index 0000000..101a63a --- /dev/null +++ b/scripts/_template/script.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# set +e # Do not exit on error +set -e # Exit on error +set +u # Allow unset variables +# set -u # Exit on unset variable +# set +o pipefail # Disable pipefail +set -o pipefail # Enable pipefail + +nc="\e[0m" # Unset styles +red="\e[31m" # Red foreground + +error() { + >&2 echo -e " ${red}×${nc} ${*}" +} + +# shellcheck disable=SC2120 +die() { + if [ "${#}" -gt 0 ]; then + error "${*}" + fi + + exit 1 +} + +main() { + echo "This is a template." +} + +main "$@" diff --git a/scripts/default.nix b/scripts/default.nix new file mode 100644 index 0000000..acce183 --- /dev/null +++ b/scripts/default.nix @@ -0,0 +1,41 @@ +{ + pkgs ? import (fetchTarball "https://github.com/nixos/nixpkgs/archive/25.05.tar.gz") { + config.allowUnfreePredicate = + pkg: + builtins.elem (pkgs.lib.getName pkg) [ + "geolite2" + ]; + }, + nix2container ? ( + import "${fetchTarball "https://github.com/nlewo/nix2container/archive/master.tar.gz"}/default.nix" + { + inherit pkgs; + inherit (pkgs) system; + } + ), + xpkgs ? pkgs.lib.packagesFromDirectoryRecursive { + inherit (pkgs) callPackage; + + directory = ../pkgs; + }, +}: + +let + xlib = import ../lib { + inherit pkgs xpkgs; + inherit (nix2container) nix2container; + }; + lib = pkgs.lib; + currentDir = ./.; + packages = lib.filterAttrs ( + name: type: + (type == "directory" && name != "_template") || (lib.hasSuffix ".nix" name && name != "default.nix") + ) (builtins.readDir currentDir); +in +lib.mapAttrs ( + name: type: + pkgs.callPackage (currentDir + "/${name}") { + inherit xlib; + inherit (nix2container) skopeo-nix2container; + } +) packages diff --git a/scripts/trivy-scan/default.nix b/scripts/trivy-scan/default.nix new file mode 100644 index 0000000..659dcb6 --- /dev/null +++ b/scripts/trivy-scan/default.nix @@ -0,0 +1,56 @@ +{ + lib, + xlib, + writeShellApplication, + coreutils, + jq, + skopeo-nix2container, + trivy, +}: + +let + name = builtins.baseNameOf (builtins.toString ./.); + allImages = xlib.mkAllImages; + allImagesImages = lib.mapAttrs (_: value: value.image) allImages; +in +writeShellApplication { + inherit name; + + text = builtins.readFile ./script.sh; + + runtimeInputs = + [ + coreutils + jq + skopeo-nix2container + trivy + ] + # Add all built images to runtime environment + ++ (lib.map (img: img.value) (lib.attrsToList allImagesImages)); + + runtimeEnv = + let + fixImgNameForEnv = name: lib.replaceStrings [ "-" ] [ "_" ] name; + + # Attribute set of all images derivation paths and tags + # Will add the following environment variables: + # - `_drv`: image store path + # - `_tag`: image tag + imagesDrvs = lib.foldlAttrs ( + acc: name: value: + let + nameInEnv = fixImgNameForEnv name; + in + acc + // { + "${nameInEnv}_drv" = value.image; + "${nameInEnv}_tag" = value.tag; + } + ) { } allImages; + in + { + # List of all images to scan + images = lib.map fixImgNameForEnv (lib.attrNames allImages); + } + // imagesDrvs; +} diff --git a/scripts/trivy-scan/script.sh b/scripts/trivy-scan/script.sh new file mode 100755 index 0000000..fbc2939 --- /dev/null +++ b/scripts/trivy-scan/script.sh @@ -0,0 +1,155 @@ +# shellcheck shell=bash + +# Global variables +readonly tmp_dir=".trivy" +readonly scans_dir="$tmp_dir/scan_results" + +# Color codes +readonly nc="\e[0m" # Unset styles +readonly red="\e[31m" # Red foreground +readonly green="\e[32m" # Green foreground +readonly yellow="\e[33m" # Yellow foreground +readonly blue="\e[34m" # Blue foreground + +# Logging functions +info() { + echo -e " ${blue}i${nc} ${*}" +} + +success() { + echo -e " ${green}✔${nc} ${*}" +} + +warn() { + echo -e " ${yellow}⚠${nc} ${*}" +} + +error() { + >&2 echo -e " ${red}×${nc} ${*}" +} + +# Cleanup function for error handling +cleanup() { + local exit_code=$? + + if [[ $exit_code -ne 0 ]]; then + error "Script failed with exit code $exit_code" + info "Cleaning up temporary files..." + rm -rf "$tmp_dir" 2>/dev/null || true + fi + + exit $exit_code +} + +# Set up error handling +trap cleanup EXIT + +# Initialize scan environment +init_scan_env() { + info "Initializing scan environment..." + + # Remove existing directory if it exists to ensure clean state + [[ -d "$tmp_dir" ]] && rm -rf "$tmp_dir" + + # Create scan directory + mkdir -p "$scans_dir" + + success "Scan directory created: $scans_dir" +} + +# Scan individual container image +scan_img() { + local img="$1" + local img_name=${img//_/-} # Replace underscores with hyphens + local drv_varname="${img}_drv" + local tag_varname="${img}_tag" + local destoci="$tmp_dir/$img_name" + local destsarif="$scans_dir/$img_name.sarif" + + info "Scanning image: $img" + + # Validate that required variables are set + if [[ -z "${!drv_varname:-}" ]]; then + error "Derivation variable $drv_varname is not set for image $img" + return 1 + fi + + if [[ -z "${!tag_varname:-}" ]]; then + error "Tag variable $tag_varname is not set for image $img" + return 1 + fi + + # Copy image derivation to OCI layout + info "Copying image derivation to OCI layout..." + if ! skopeo --insecure-policy copy nix:"${!drv_varname}" oci:"$destoci"; then + error "Failed to copy image derivation for $img" + return 1 + fi + + # Scan image for vulnerabilities and generate SARIF report + info "Scanning for vulnerabilities..." + if ! trivy image --input "$destoci" --scanners vuln -f sarif -o "$destsarif"; then + error "Trivy scan failed for $img" + return 1 + fi + + # Update SARIF report with actual image tag information + info "Updating SARIF report with image tag information..." + if [[ -f "$destsarif" ]]; then + # Replace OCI path with actual tag name in scan results + sed -i "s|$destoci|${!tag_varname}|" "$destsarif" + # Set proper repository tags in SARIF metadata + sed -i 's/"repoTags": null/"repoTags": ["'"${!tag_varname}"'"]/' "$destsarif" + + success "Scan completed for $img -> $destsarif" + else + error "SARIF report not generated for $img" + return 1 + fi +} + +main() { + info "Starting container vulnerability scanning process..." + + # Check if images array is defined and not empty + if [[ -z "${images:-}" ]] || [[ ${#images[@]} -eq 0 ]]; then + error "No images defined in 'images' array" + warn "Please define an array of image names before running this script" + exit 1 + fi + + # Initialize scanning environment + init_scan_env + + # Scan all images in the array + info "Scanning ${#images[@]} images..." + local failed_scans=0 + + # shellcheck disable=SC2154 + for img in "${images[@]}"; do + if ! scan_img "$img"; then + ((failed_scans++)) + error "Failed to scan image: $img" + fi + done + + # Report scan summary + local successful_scans=$((${#images[@]} - failed_scans)) + info "Scan summary: $successful_scans successful, $failed_scans failed" + + if [[ $failed_scans -gt 0 ]]; then + warn "Some scans failed. Check the logs above for details." + fi + + # Cleanup intermediate OCI directories (keep SARIF files) + info "Cleaning up intermediate files..." + find "$tmp_dir" -mindepth 1 -maxdepth 1 -type d -not -path "$scans_dir" -exec rm -rf {} \; 2>/dev/null || true + + success "Vulnerability scanning completed successfully!" + success "Results available in: $tmp_dir" +} + +# Execute main function if script is run directly (not sourced) +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/shell.nix b/shell.nix index d338837..51780f9 100644 --- a/shell.nix +++ b/shell.nix @@ -1,17 +1,41 @@ { - pkgs ? import (fetchTarball "https://github.com/nixos/nixpkgs/archive/25.05.tar.gz") { }, + pkgs ? import (fetchTarball "https://github.com/nixos/nixpkgs/archive/25.05.tar.gz") { + config.allowUnfreePredicate = + pkg: + builtins.elem (pkgs.lib.getName pkg) [ + "geolite2" + ]; + }, + nix2container ? ( + import "${fetchTarball "https://github.com/nlewo/nix2container/archive/master.tar.gz"}/default.nix" + { + inherit pkgs; + inherit (pkgs) system; + } + ), + xpkgs ? pkgs.lib.packagesFromDirectoryRecursive { + inherit (pkgs) callPackage; + + directory = ./pkgs; + }, }: let - inherit (pkgs) mkShellNoCC; + inherit (pkgs) mkShellNoCC lib; + + scripts = lib.map (img: img.value) ( + lib.attrsToList (import ./scripts { inherit pkgs xpkgs nix2container; }) + ); in mkShellNoCC { - nativeBuildInputs = with pkgs; [ - bash - coreutils - findutils - mmdbinspect - ]; + nativeBuildInputs = + (with pkgs; [ + bash + coreutils + findutils + mmdbinspect + ]) + ++ scripts; shellHook = '' find .hooks \