diff --git a/.github/workflows/build-docker-container.yml b/.github/workflows/build-docker-container.yml new file mode 100644 index 0000000000..36ce50c81e --- /dev/null +++ b/.github/workflows/build-docker-container.yml @@ -0,0 +1,82 @@ +--- +name: Build and Publish Docker Container +on: + workflow_dispatch: + inputs: + docker-name: + required: true + type: string + default: fac + image-name: + required: true + type: string + default: web-container + repo-name: + required: true + type: string + default: gsa-tts/fac + work-dir: + required: true + type: string + default: ./backend + workflow_call: + inputs: + docker-name: + required: true + type: string + default: fac + image-name: + required: true + type: string + default: web-container + repo-name: + required: true + type: string + default: gsa-tts/fac + work-dir: + required: true + type: string + default: ./backend + +env: + DOCKER_NAME: ${{ inputs.docker-name }} + IMAGE: ${{ inputs.image-name }} + GH_REPO: ${{ inputs.repo-name }} + WORKING_DIRECTORY: ${{ inputs.work-dir }} + +jobs: + build-with-docker: + name: Build Docker Container + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Get Date + shell: bash + id: date + run: echo "date=$(date +%Y%m%d)" >> $GITHUB_OUTPUT + + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Build Container + working-directory: ${{ env.WORKING_DIRECTORY }} + run: docker build -t ${{ env.DOCKER_NAME }}:${{ steps.date.outputs.date }} . + + - name: Tag Image + run: docker tag ${{ env.DOCKER_NAME }}:${{ steps.date.outputs.date }} ghcr.io/${{ env.GH_REPO }}/${{ env.IMAGE }}:latest + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Push Web Container + run: docker push --all-tags ghcr.io/${{ env.GH_REPO }}/${{ env.IMAGE }} diff --git a/.github/workflows/staging-scheduled-deploy.yml b/.github/workflows/staging-scheduled-deploy.yml index 3b8d7c1565..5df4153437 100644 --- a/.github/workflows/staging-scheduled-deploy.yml +++ b/.github/workflows/staging-scheduled-deploy.yml @@ -1,10 +1,46 @@ --- - name: Scheduled Deploy From Main to Staging - on: - schedule: - - cron: '30 4 * * *' - jobs: - create-pr: - name: Auto Create PR at 430am UTC Daily - uses: ./.github/workflows/auto-create-pr.yml - secrets: inherit +name: Scheduled Deploy From Main to Staging +on: + schedule: + - cron: '0 10 * * 1-5' + workflow_dispatch: +jobs: + trivy-scan: + uses: ./.github/workflows/trivy.yml + secrets: inherit + permissions: + contents: read + packages: write + actions: read + security-events: write + with: + work-dir: ./backend + docker-name: fac + + build-container: + needs: + - trivy-scan + uses: ./.github/workflows/build-docker-container.yml + secrets: inherit + permissions: + contents: read + packages: write + with: + docker-name: fac + image-name: web-container + repo-name: gsa-tts/fac + work-dir: ./backend + + test-and-lint: + name: Run Django, Lighthouse, a11y and lint + needs: + - build-container + uses: ./.github/workflows/test.yml + secrets: inherit + + create-pr: + needs: + - test-and-lint + name: Create Pull Request to Staging + uses: ./.github/workflows/auto-create-pr.yml + secrets: inherit diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef23a19b1c..97bfb18059 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,7 @@ --- -name: Runs linting and tests +name: Run Testing and Linting on: + workflow_dispatch: workflow_call: jobs: @@ -56,11 +57,11 @@ jobs: - name: Run HTML template linting working-directory: ./backend run: djlint --lint . + frontend-linting: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Restore npm cache uses: actions/cache@v3 id: cache-npm @@ -79,13 +80,14 @@ jobs: - name: Lint JS & SCSS working-directory: ./backend run: npm run check-all - test: + + django-test: runs-on: ubuntu-latest env: ENV: TESTING - SAM_API_KEY: ${{ secrets.SAM_API_KEY }}" + SAM_API_KEY: ${{ secrets.SAM_API_KEY }} DJANGO_BASE_URL: 'http://localhost:8000' - DJANGO_SECRET_LOGIN_KEY: ${{ secrets.DJANGO_SECRET_LOGIN_KEY }}" + DJANGO_SECRET_LOGIN_KEY: ${{ secrets.DJANGO_SECRET_LOGIN_KEY }} SECRET_KEY: ${{ secrets.SECRET_KEY }} ALLOWED_HOSTS: '0.0.0.0 127.0.0.1 localhost' DISABLE_AUTH: False @@ -94,24 +96,24 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 16 - - name: Pull Docker Hub images + - name: Create .env file working-directory: ./backend - run: touch .env && docker-compose pull - - name: Start services + run: touch .env + - name: Start Services working-directory: ./backend - run: docker-compose up -d + run: docker compose -f docker-compose-web.yml up -d - name: Run Django test suite working-directory: ./backend - run: - docker-compose run web bash -c 'coverage run --parallel-mode --concurrency=multiprocessing manage.py test --parallel && - coverage combine && coverage report -m --fail-under=90' + run: | + docker compose -f docker-compose-web.yml run web bash -c 'coverage run --parallel-mode --concurrency=multiprocessing manage.py test --parallel && coverage combine && coverage report -m --fail-under=90' + a11y-testing: runs-on: ubuntu-20.04 env: ENV: TESTING - SAM_API_KEY: ${{ secrets.SAM_API_KEY }}" + SAM_API_KEY: ${{ secrets.SAM_API_KEY }} DJANGO_BASE_URL: 'http://localhost:8000' - DJANGO_SECRET_LOGIN_KEY: ${{ secrets.DJANGO_SECRET_LOGIN_KEY }}" + DJANGO_SECRET_LOGIN_KEY: ${{ secrets.DJANGO_SECRET_LOGIN_KEY }} SECRET_KEY: ${{ secrets.SECRET_KEY }} ALLOWED_HOSTS: '0.0.0.0 127.0.0.1 localhost' DISABLE_AUTH: True @@ -120,12 +122,12 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 16 - - name: Pull Docker Hub images + - name: Create .env file working-directory: ./backend - run: touch .env && docker-compose pull - - name: Start services + run: touch .env + - name: Start Services working-directory: ./backend - run: docker-compose up -d + run: docker compose -f docker-compose-web.yml up -d - name: run Lighthouse CI run: | npm install -g @lhci/cli@0.8.x @@ -134,5 +136,6 @@ jobs: run: | npm i -g pa11y-ci pa11y-ci + validate-terraform: uses: ./.github/workflows/terraform-lint.yml diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml new file mode 100644 index 0000000000..6d23699965 --- /dev/null +++ b/.github/workflows/trivy.yml @@ -0,0 +1,68 @@ +--- +name: Trivy Scan +on: + workflow_dispatch: + inputs: + docker-name: + required: true + type: string + default: fac + work-dir: + required: true + type: string + default: ./backend + workflow_call: + inputs: + docker-name: + required: true + type: string + default: fac + work-dir: + required: true + type: string + default: ./backend + +permissions: + contents: read + +env: + DOCKER_NAME: ${{ inputs.docker-name }} + WORKING_DIRECTORY: ${{ inputs.work-dir }} + +jobs: + trivy: + permissions: + contents: read + security-events: write + actions: read + name: Trivy Scan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Get Date + shell: bash + id: date + run: | + echo "date=$(date +%Y%m%d%H%M%S)" >> $GITHUB_OUTPUT + + - name: Build Container + working-directory: ${{ env.WORKING_DIRECTORY }} + run: docker build -t ${{ env.DOCKER_NAME }}:${{ steps.date.outputs.date }} . + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: '${{ env.DOCKER_NAME }}:${{ steps.date.outputs.date }}' + scan-type: 'image' + hide-progress: false + format: 'sarif' + output: 'trivy-results.sarif' + exit-code: '1' + ignore-unfixed: true + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: 'trivy-results.sarif' diff --git a/backend/.profile b/backend/.profile index 1c0dc06828..5286597105 100644 --- a/backend/.profile +++ b/backend/.profile @@ -7,6 +7,13 @@ export https_proxy="$(echo "$VCAP_SERVICES" | jq --raw-output --arg service_name export smtp_proxy_domain="$(echo "$VCAP_SERVICES" | jq --raw-output --arg service_name "smtp-proxy-creds" ".[][] | select(.name == \$service_name) | .credentials.domain")" export smtp_proxy_port="$(echo "$VCAP_SERVICES" | jq --raw-output --arg service_name "smtp-proxy-creds" ".[][] | select(.name == \$service_name) | .credentials.port")" +S3_ENDPOINT_FOR_NO_PROXY="$(echo $VCAP_SERVICES | jq --raw-output --arg service_name "fac-public-s3" ".[][] | select(.name == \$service_name) | .credentials.endpoint")" +S3_FIPS_ENDPOINT_FOR_NO_PROXY="$(echo $VCAP_SERVICES | jq --raw-output --arg service_name "fac-public-s3" ".[][] | select(.name == \$service_name) | .credentials.fips_endpoint")" +S3_PRIVATE_ENDPOINT_FOR_NO_PROXY="$(echo $VCAP_SERVICES | jq --raw-output --arg service_name "fac-private-s3" ".[][] | select(.name == \$service_name) | .credentials.endpoint")" +S3_PRIVATE_FIPS_ENDPOINT_FOR_NO_PROXY="$(echo $VCAP_SERVICES | jq --raw-output --arg service_name "fac-private-s3" ".[][] | select(.name == \$service_name) | .credentials.fips_endpoint")" +export no_proxy="${S3_ENDPOINT_FOR_NO_PROXY},${S3_FIPS_ENDPOINT_FOR_NO_PROXY},${S3_PRIVATE_ENDPOINT_FOR_NO_PROXY},${S3_PRIVATE_FIPS_ENDPOINT_FOR_NO_PROXY},apps.internal" + + # Grab the New Relic license key from the newrelic-creds user-provided service instance export NEW_RELIC_LICENSE_KEY="$(echo "$VCAP_SERVICES" | jq --raw-output --arg service_name "newrelic-creds" ".[][] | select(.name == \$service_name) | .credentials.NEW_RELIC_LICENSE_KEY")" diff --git a/backend/audit/storages.py b/backend/audit/storages.py new file mode 100644 index 0000000000..b81616cdf4 --- /dev/null +++ b/backend/audit/storages.py @@ -0,0 +1,13 @@ +from django.conf import settings +from storages.backends.s3boto3 import S3Boto3Storage + + +class PrivateS3Storage(S3Boto3Storage): + """ + Our S3 settings as a storage class. + """ + + bucket_name = settings.AWS_PRIVATE_STORAGE_BUCKET_NAME + access_key = settings.AWS_PRIVATE_ACCESS_KEY_ID + secret_key = settings.AWS_PRIVATE_SECRET_ACCESS_KEY + location = "" diff --git a/backend/audit/views.py b/backend/audit/views.py index 6a017b280c..72ee2adf7a 100644 --- a/backend/audit/views.py +++ b/backend/audit/views.py @@ -24,7 +24,7 @@ CertifyingAuditorRequiredMixin, SingleAuditChecklistAccessRequiredMixin, ) -from audit.models import ExcelFile, SingleAuditChecklist +from audit.models import Access, ExcelFile, SingleAuditChecklist from audit.validators import ( validate_federal_award_json, validate_corrective_action_plan_json, @@ -53,18 +53,17 @@ def get(self, request, *args, **kwargs): @classmethod def fetch_my_submissions(cls, user): - data = ( - SingleAuditChecklist.objects.all() - .values( - "report_id", - "submission_status", - auditee_uei=F("general_information__auditee_uei"), - auditee_name=F("general_information__auditee_name"), - fiscal_year_end_date=F( - "general_information__auditee_fiscal_period_end" - ), - ) - .filter(submitted_by=user) + """ + Get all submissions the user is associated with via Access objects. + """ + accesses = Access.objects.filter(user=user) + sac_ids = [access.sac.id for access in accesses] + data = SingleAuditChecklist.objects.filter(id__in=sac_ids).values( + "report_id", + "submission_status", + auditee_uei=F("general_information__auditee_uei"), + auditee_name=F("general_information__auditee_name"), + fiscal_year_end_date=F("general_information__auditee_fiscal_period_end"), ) return data diff --git a/backend/config/settings.py b/backend/config/settings.py index 561d63f884..9ea8ff465c 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -233,7 +233,7 @@ else: # One of the Cloud.gov environments STATICFILES_STORAGE = "storages.backends.s3boto3.S3ManifestStaticStorage" - DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" + DEFAULT_FILE_STORAGE = "report_submission.storages.S3PrivateStorage" vcap = json.loads(env.str("VCAP_SERVICES")) for service in vcap["s3"]: if service["instance_name"] == "fac-public-s3": @@ -258,7 +258,6 @@ STATIC_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/{AWS_LOCATION}/" STATICFILES_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" - DEFAULT_FILE_STORAGE = "cts_forms.storages.PrivateS3Storage" AWS_IS_GZIPPED = True elif service["instance_name"] == "fac-private-s3": @@ -280,7 +279,9 @@ AWS_PRIVATE_DEFAULT_ACL = "private" # If wrong, https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl - MEDIA_URL = f"https://{AWS_S3_PRIVATE_CUSTOM_DOMAIN}/{AWS_LOCATION}/" + MEDIA_URL = ( + f"https://{AWS_S3_PRIVATE_CUSTOM_DOMAIN}/{AWS_PRIVATE_LOCATION}/" + ) # secure headers MIDDLEWARE.append("csp.middleware.CSPMiddleware") diff --git a/backend/docker-compose-web.yml b/backend/docker-compose-web.yml new file mode 100644 index 0000000000..71c178b9e0 --- /dev/null +++ b/backend/docker-compose-web.yml @@ -0,0 +1,71 @@ +--- +version: "3.7" + +services: + db: + image: "postgres:12" + environment: + - "POSTGRES_HOST_AUTH_METHOD=trust" + volumes: + - postgres-data:/var/lib/postgresql/data/ + ports: + - "5432:5432" + + web: + image: ghcr.io/gsa-tts/fac/web-container:latest + command: /src/run.sh + depends_on: + - db + - minio + environment: + - "DATABASE_URL=postgres://postgres@db/postgres" + - "DJANGO_DEBUG=true" + - "SAM_API_KEY=${SAM_API_KEY}" + - "DJANGO_BASE_URL=http://localhost:8000" + - "DJANGO_SECRET_LOGIN_KEY=${DJANGO_SECRET_LOGIN_KEY}" + - "ENV=${ENV}" + - "SECRET_KEY=${SECRET_KEY}" + - "ALLOWED_HOSTS=0.0.0.0 127.0.0.1 localhost" + - "AV_SCAN_URL=http://clamav-rest:9000/scan" + - "DISABLE_AUTH=${DISABLE_AUTH:-False}" + - "LOCALSTACK_HOST=localstack" + env_file: + - ".env" + ports: + - "8000:8000" + volumes: + - .:/src + - /src/node_modules + - /src/staticfiles + clamav-rest: + image: ghcr.io/gsa-tts/fac/clamav:latest + environment: + - MAX_FILE_SIZE=25M + - SIGNATURE_CHECKS=1 + ports: + - "9000:9000" + minio: + container_name: "minio" + image: minio/minio + command: server /tmp/minio --console-address ":9002" + ports: + - "9001:9000" + - "9002:9002" + volumes: + - "minio-vol:/tmp/minio" + api: + image: ghcr.io/gsa-tts/fac/postgrest:latest + ports: + - "3000:3000" + expose: + - "3000" + environment: + PGRST_DB_URI: postgres://postgres@db:5432/postgres + PGRST_OPENAPI_SERVER_PROXY_URI: http://127.0.0.1:3000 + PGRST_DB_ANON_ROLE: anon + PGRST_DB_SCHEMAS: api + depends_on: + - db +volumes: + postgres-data: + minio-vol: