diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..33a608996 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.git +.github +.gradle +.agents +.settings +.vscode +BOOT-INF +WEB-INF +bin +config +docker +gradle +node_modules +build +log +report.html +report-docker.html \ No newline at end of file diff --git a/.github/workflows/makefile.yml b/.github/workflows/makefile.yml index e53a8acf3..002080e45 100644 --- a/.github/workflows/makefile.yml +++ b/.github/workflows/makefile.yml @@ -22,4 +22,4 @@ jobs: java-version: '17' - name: Run Tests for PR - run: make test \ No newline at end of file + run: make test diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index 0690cfbe5..cc4e31a16 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -10,54 +10,101 @@ on: - develop-* - master - workflow_dispatch: # Allows manual runs from the GitHub UI + workflow_dispatch: # Allows manual runs from the GitHub UI jobs: trivy-scan: runs-on: ubuntu-latest - steps: - # Checkout the pull request code - - name: Checkout code - uses: actions/checkout@v4 - - # Set up Java 17 - - name: Set up Java 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' # Use Eclipse Temurin distribution for Java - - # Install Trivy if not cached - - name: Install Trivy - run: | - TRIVY_LATEST_VERSION=$(curl --silent "https://api.github.com/repos/aquasecurity/trivy/releases/latest" | jq -r .tag_name) - wget https://github.com/aquasecurity/trivy/releases/download/${TRIVY_LATEST_VERSION}/trivy_${TRIVY_LATEST_VERSION#v}_Linux-64bit.deb - sudo dpkg -i trivy_${TRIVY_LATEST_VERSION#v}_Linux-64bit.deb - - # Run Trivy to scan the repository or container image - - name: Run Trivy scan - id: trivy-scan - run: | - ./gradlew dependencies --write-locks - trivy fs gradle.lockfile --format json --output trivy_report.json --severity HIGH,CRITICAL - /bin/rm -rf gradle/dependency-locks - /bin/rm gradle.lockfile - env: - TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db - TRIVY_JAVA_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-java-db - - # Check for high or critical vulnerabilities and output if found - - name: Check and output vulnerabilities - id: check-vulnerabilities - run: | - vuln_count=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH" or .Severity == "CRITICAL")] | length' trivy_report.json) - echo "Vulnerability count: $vuln_count" - if [ "$vuln_count" -gt 0 ]; then - echo "High or Critical vulnerabilities found!" - jq -r '.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH" or .Severity == "CRITICAL") | "\(.VulnerabilityID): \(.PkgName) - Severity: \(.Severity) - Installed Version: \(.InstalledVersion) - Fixed Version: \(.FixedVersion)"' trivy_report.json - exit 1 - else - echo "No high or critical vulnerabilities found." - fi + # Checkout code + - name: Checkout code + uses: actions/checkout@v4 + + # Set up Java 17 + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + # Generate dependency lock for scanning + - name: Generate dependency lock for scanning + run: ./gradlew dependencies --write-locks + + # Install Trivy + - name: Install Trivy + run: | + TRIVY_LATEST_VERSION=$(curl --silent "https://api.github.com/repos/aquasecurity/trivy/releases/latest" | jq -r .tag_name) + wget https://github.com/aquasecurity/trivy/releases/download/${TRIVY_LATEST_VERSION}/trivy_${TRIVY_LATEST_VERSION#v}_Linux-64bit.deb + sudo dpkg -i trivy_${TRIVY_LATEST_VERSION#v}_Linux-64bit.deb + + # Run Trivy scan + - name: Run Trivy scan + id: trivy-scan + run: trivy fs gradle.lockfile --scanners vuln --severity HIGH,CRITICAL --format json --output trivy_report.json --exit-code 0 + env: + TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db + TRIVY_JAVA_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-java-db + + # Check and output vulnerabilities + - name: Check and output vulnerabilities + id: check-vulnerabilities + run: | + vuln_count=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH" or .Severity == "CRITICAL")] | length' trivy_report.json) + echo "Vulnerability count: $vuln_count" + if [ "$vuln_count" -gt 0 ]; then + echo "High or Critical vulnerabilities found!" + jq -r '.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH" or .Severity == "CRITICAL") | "\(.VulnerabilityID): \(.PkgName) - Severity: \(.Severity) - Installed Version: \(.InstalledVersion) - Fixed Version: \(.FixedVersion)"' trivy_report.json + exit 1 + else + echo "No high or critical vulnerabilities found." + fi + + # Remove generated dependency locks + - name: Remove generated dependency locks + if: always() + run: | + rm -rf gradle/dependency-locks + rm -f gradle.lockfile + image-scan: + runs-on: ubuntu-latest + + steps: + # Checkout code + - name: Checkout code + uses: actions/checkout@v4 + + + # Build application image + - name: Build application image + run: make docker DOCKER_IMAGE=evsrestapi:ci + + # Install Trivy + - name: Install Trivy + run: | + TRIVY_LATEST_VERSION=$(curl --silent "https://api.github.com/repos/aquasecurity/trivy/releases/latest" | jq -r .tag_name) + wget https://github.com/aquasecurity/trivy/releases/download/${TRIVY_LATEST_VERSION}/trivy_${TRIVY_LATEST_VERSION#v}_Linux-64bit.deb + sudo dpkg -i trivy_${TRIVY_LATEST_VERSION#v}_Linux-64bit.deb + + # Scan application image + - name: Scan application image + id: image-trivy-scan + run: trivy image evsrestapi:ci --scanners vuln --severity HIGH,CRITICAL --format json --output trivy_image_report.json --exit-code 0 + env: + TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db + TRIVY_JAVA_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-java-db + + # Check and output image vulnerabilities + - name: Check and output image vulnerabilities + id: check-image-vulnerabilities + run: | + vuln_count=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH" or .Severity == "CRITICAL")] | length' trivy_image_report.json) + echo "Image vulnerability count: $vuln_count" + if [ "$vuln_count" -gt 0 ]; then + echo "High or Critical image vulnerabilities found!" + jq -r '.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH" or .Severity == "CRITICAL") | "\(.VulnerabilityID): \(.PkgName) - Severity: \(.Severity) - Installed Version: \(.InstalledVersion) - Fixed Version: \(.FixedVersion)"' trivy_image_report.json + exit 1 + else + echo "No high or critical image vulnerabilities found." + fi diff --git a/.gitignore b/.gitignore index 666b2082b..feaf7cc11 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,4 @@ UnitTestData/* src/main/main.iml src/test/test.iml report-docker.html -.docker-secrets/ +/.docker-secrets/ diff --git a/Dockerfile b/Dockerfile index 884787262..fd7cd7ae4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,38 +1,26 @@ -# Base image -FROM centos:7 -MAINTAINER Frankie Parks +# Build the deployable WAR in an isolated Linux Gradle environment. +FROM gradle:8.14.2-jdk17 AS gradle-build -ENV TZ=America/New_York -RUN ln -snf "/usr/share/zoneinfo/$TZ" /etc/localtime && echo "$TZ" > /etc/timezone +WORKDIR /workspace -# Update all packages installed for security -RUN yum makecache && yum update -y +COPY --chown=gradle:gradle build.gradle gradle.properties ./ +RUN gradle --no-daemon dependencies -# Create a user and group used to launch processes -# The user ID 1000 is the default for the first "regular" user on Fedora/RHEL, -# so there is a high chance that this ID will be equal to the current user -# making it easier to use volumes (no permission issues) -RUN groupadd -r evsapi -g 1000 && useradd -u 1000 -r -g evsapi -m -d /opt/evsapi -s /sbin/nologin -c "EVSAPI user" evsapi && \ - chmod 755 /opt/evsapi +COPY --chown=gradle:gradle src/main ./src/main +RUN gradle --no-daemon bootWar -x test -# Set the working directory to evsapi' user home directory -WORKDIR /opt/evsapi +# Run only the packaged application as an unprivileged user. +FROM eclipse-temurin:17-jre-jammy -# Install necessary packages -RUN yum -y install java-1.8.0-openjdk-devel unzip && yum clean all +WORKDIR /app -# Set ENV variable for EVS_SERVER_PORT -ENV EVS_SERVER_PORT="5830" -ENV JAVA_OPTIONS="-Xmx2048m -XX:PermSize=1024m -XX:MaxPermSize=512m" +RUN groupadd --system evsapi \ + && useradd --system --gid evsapi --home-dir /app --shell /usr/sbin/nologin evsapi -# Add file files to image -ADD build/distributions/evsrestapi-1.0.0-SNAPSHOT.zip /opt/evsapi/ -RUN unzip evsrestapi-1.0.0-SNAPSHOT.zip -RUN ln -s evsrestapi-1.0.0-SNAPSHOT evsrestapi +COPY --from=gradle-build --chown=evsapi:evsapi /workspace/build/libs/evsrestapi-*.war /app/evsrestapi.war -RUN ls -l - -EXPOSE 5830 USER evsapi -CMD java -jar ./evsrestapi/lib/evsrestapi.war +EXPOSE 8082 + +ENTRYPOINT ["java", "-jar", "/app/evsrestapi.war"] \ No newline at end of file diff --git a/Makefile b/Makefile index 301efb10c..6c0e4c79a 100644 --- a/Makefile +++ b/Makefile @@ -14,23 +14,95 @@ GIT_COMMIT ?= $(shell echo `git log | grep -m1 -oE '[^ ]+$'`) GIT_COMMITTED_AT ?= $(shell echo `git log -1 --format=%ct`) GIT_BRANCH ?= FULL_VERSION := v$(APP_VERSION)-g$(GIT_VERSION) +DOCKER_TAG := $(shell grep "^version =" build.gradle | sed 's/version = //; s/"//g; s/.RELEASE//') +DOCKER_IMAGE ?= $(SERVICE):$(DOCKER_TAG) +ES_PORT ?= 9201 +ES_SCHEME ?= http +GRAPH_DB_PORT ?= 3030 +GRAPH_DB ?= NCIT2 +DOCKER_PORT ?= 8082 +DOCKER_ES_HOST ?= host.docker.internal +DOCKER_GRAPH_DB_HOST ?= host.docker.internal +DOCKER_SECRETS_DIR ?= $(CURDIR)/.docker-secrets +DOCKER_IMAGE_STAMP := build/.docker-image-$(subst :,_,$(subst /,_,$(DOCKER_IMAGE))) +DOCKER_BUILD_INPUTS := Dockerfile .dockerignore build.gradle gradle.properties $(shell git ls-files --cached --others --exclude-standard src/main) -.PHONY: build +GRADLEW ?= ./gradlew + +ifeq ($(OS),Windows_NT) +DOCKER ?= docker.exe +DOCKER_HOST_GATEWAY_ARG := +else +DOCKER ?= docker +DOCKER_HOST_GATEWAY_ARG := --add-host host.docker.internal:host-gateway +endif + +.PHONY: build docker dockerpush scandocker rundocker check-docker-secrets scan # consider also "docker save..." and "docker load..." to avoid registry. clean: - ./gradlew clean + $(GRADLEW) clean # Build the library without tests # On Windows use: git config core.eol lf build: - ./gradlew clean spotlessApply build spotbugsMain spotbugsTest -x test -x zipFile + $(GRADLEW) clean spotlessApply build spotbugsMain spotbugsTest -x test -x zipFile run: build java -Dspring.profiles.active=local -jar build/libs/evsrestapi*.war +# Build the application and image in an isolated Linux/AMD64 Docker build environment. +docker: $(DOCKER_IMAGE_STAMP) + @$(DOCKER) image inspect "$(DOCKER_IMAGE)" > /dev/null 2>&1 || { rm -f "$(DOCKER_IMAGE_STAMP)"; $(MAKE) --no-print-directory "$(DOCKER_IMAGE_STAMP)"; } + @echo "Docker image $(DOCKER_IMAGE) is up to date." + +$(DOCKER_IMAGE_STAMP): $(DOCKER_BUILD_INPUTS) + $(DOCKER) build --platform linux/amd64 --tag "$(DOCKER_IMAGE)" . + @mkdir -p "$(dir $@)" + @touch "$@" + +# Build and push a Linux/AMD64 image. Override DOCKER_IMAGE with a registry-qualified image name. +dockerpush: + $(DOCKER) buildx build --platform linux/amd64 --tag "$(DOCKER_IMAGE)" --push . + +# Report all HIGH and CRITICAL image vulnerabilities with their installed and fixed versions. +# The complete HTML report is written to report-docker.html. +scandocker: docker + trivy image "$(DOCKER_IMAGE)" --scanners vuln --severity HIGH,CRITICAL --format table + trivy image "$(DOCKER_IMAGE)" --scanners vuln --format template -o report-docker.html --template "@config/trivy/html.tpl" + + + +# Require a local, ignored directory of Spring Boot config-tree secret files. +check-docker-secrets: + @test -d "$(DOCKER_SECRETS_DIR)" || (echo "ERROR: Create $(DOCKER_SECRETS_DIR) and add secret files before running rundocker." && exit 1) + +# Run against Jena/Fuseki and OpenSearch services exposed on the Docker host. +# Secrets are mounted read-only and imported from /run/secrets rather than passed as environment variables. +# Override DOCKER_ES_HOST, DOCKER_GRAPH_DB_HOST, ports, or the secrets directory as needed. +rundocker: docker check-docker-secrets + $(DOCKER) run --rm --name "$(SERVICE)" -p "$(DOCKER_PORT):8082" $(DOCKER_HOST_GATEWAY_ARG) \ + --mount type=bind,src="$(DOCKER_SECRETS_DIR)",dst=/run/secrets,readonly \ + -e SPRING_CONFIG_IMPORT=optional:configtree:/run/secrets/ \ + -e SPRING_PROFILES_ACTIVE=local \ + -e EVS_SERVER_PORT=8082 \ + -e ES_HOST="$(DOCKER_ES_HOST)" \ + -e ES_PORT="$(ES_PORT)" \ + -e ES_SCHEME="$(ES_SCHEME)" \ + -e GRAPH_DB_HOST="$(DOCKER_GRAPH_DB_HOST)" \ + -e GRAPH_DB_PORT="$(GRAPH_DB_PORT)" \ + -e GRAPH_DB="$(GRAPH_DB)" \ + -e CONFIG_BASE_URI \ + -e MAIL_HOST \ + -e MAIL_PORT \ + -e MAIL_AUTH \ + -e MAIL_TLS \ + -e MAIL_RECIPIENT \ + -e RECAPTCHA_KEY \ + "$(DOCKER_IMAGE)" + test: - ./gradlew spotlessCheck -x test + $(GRADLEW) spotlessCheck -x test releasetag: git tag -a "${VERSION}-RC-`/bin/date +%Y-%m-%d`" -m "Release ${VERSION}-RC-`/bin/date +%Y-%m-%d`" @@ -54,9 +126,11 @@ version: devreset: build ./src/main/bin/devreset.sh ../data/UnitTestData > log 2>&1 & +# Report all HIGH and CRITICAL dependency vulnerabilities with installed and fixed versions. +# The complete HTML report is written to report.html. Generated dependency locks are always removed. scan: - ./gradlew dependencies --write-locks - trivy fs gradle.lockfile --format template -o report.html --template "@config/trivy/html.tpl" - grep CRITICAL report.html - /bin/rm -rf gradle/dependency-locks - /bin/rm gradle.lockfile + @set -e; \ + trap 'rm -rf gradle/dependency-locks gradle.lockfile' EXIT; \ + $(GRADLEW) dependencies --write-locks; \ + trivy fs gradle.lockfile --scanners vuln --severity HIGH,CRITICAL --format table; \ + trivy fs gradle.lockfile --scanners vuln --format template -o report.html --template "@config/trivy/html.tpl" diff --git a/README.md b/README.md index d9593fff0..2ca9f2201 100644 --- a/README.md +++ b/README.md @@ -83,3 +83,21 @@ Information on the build and deployment process for the EVSRESTAPI project ### Run application from command line * Run with `java -Xmx4096 -Dspring.profiles.active=local -jar build/libs/evsrestapi*.jar` + +### Build, scan, and run the application image + +* `make docker` builds the WAR inside a Linux/AMD64 Docker build stage and creates `evsrestapi:` without using a local Gradle installation. +* The image starts the executable WAR, which runs the REST API entry point; the executable JAR is reserved for loader and reindex operations. +* `make scandocker` and `make scan` print HIGH/CRITICAL vulnerability tables and write full HTML reports. GitHub Actions enforces HIGH/CRITICAL findings in CI. +* `make dockerpush DOCKER_IMAGE=/:` builds and pushes a Linux/AMD64 image with Docker Buildx. +* `make rundocker` runs the image on port 8082 using the `local` Spring profile. It assumes Jena/Fuseki and OpenSearch are already running on the host, uses `host.docker.internal` on Docker Desktop, and adds the host-gateway mapping automatically on Linux. +* `make rundocker` is the supported way to start the image locally. A direct `docker run` must supply equivalent Spring profile, port, service-host, and secret configuration; otherwise the image uses the default application settings rather than the local setup. +* Before running it, create the ignored `.docker-secrets` directory. Each file is mounted read-only at `/run/secrets` and is imported by Spring Boot using its filename as the property name. Put credentials and secrets in `NCI_EVS_ADMIN_KEY`, `MAIL_USER`, `MAIL_PASSWORD`, and `RECAPTCHA_SECRET`; write each value without a trailing newline. Non-sensitive settings such as `MAIL_HOST`, `MAIL_PORT`, and `RECAPTCHA_KEY` continue to be forwarded from the host environment. +* The secret values are not passed as container environment variables, so they do not appear in `docker inspect`. Docker daemon administrators can still access a running container and must remain trusted. +* Override the service hosts or published port when necessary, for example: + + ```bash + make rundocker DOCKER_ES_HOST=host.docker.internal DOCKER_GRAPH_DB_HOST=host.docker.internal DOCKER_PORT=8082 + ``` + + The existing `ES_PORT`, `ES_SCHEME`, `GRAPH_DB_PORT`, and `GRAPH_DB` settings are passed through to the container. Email, reCAPTCHA, and other applicable local configuration environment variables are also forwarded. diff --git a/build.gradle b/build.gradle index a4c8aad27..e6e58ac1b 100644 --- a/build.gradle +++ b/build.gradle @@ -103,7 +103,7 @@ dependencies { // Use version 2 with spring boot 3 // custom location => springdoc.swagger-ui.path=/swagger-ui/index.html - implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0" + implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.17" implementation "org.springframework.boot:spring-boot-devtools" implementation "org.springframework.boot:spring-boot-starter-actuator" @@ -142,6 +142,12 @@ dependencies { //Java Mail Sender dependency implementation "org.springframework.boot:spring-boot-starter-mail" + // Security override: CVE-2026-41695 + implementation "org.springframework.data:spring-data-commons:3.5.12" + + // Security override: CVE-2026-41006 and CVE-2026-41007. + implementation "org.springframework.hateoas:spring-hateoas:2.5.3" + // aspectjweaver is now managed by Spring Boot BOM (1.9.25.1 in 3.5.x) implementation "org.aspectj:aspectjweaver" implementation "org.apache.commons:commons-text:1.10.0"