diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..610b0005d --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,21 @@ +version: 2 +jobs: + build: + docker: + - image: circleci/python:3.6 + steps: + - checkout + - restore_cache: + key: deps1-{{ .Branch }}-{{ checksum "requirements.txt" }} + - run: + command: | + python3 -m venv venv + . venv/bin/activate + pip install -r requirements.txt + - save_cache: + key: deps1-{{ .Branch }}-{{ checksum "requirements.txt" }} + paths: + - "venv" + - store_artifacts: + path: test-reports/ + destination: python_app \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..03a268b8b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +**/.DS_Store +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose.y*ml +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/.github/workflows/build-and-push-to-docker-hub.yml b/.github/workflows/build-and-push-to-docker-hub.yml new file mode 100644 index 000000000..42f040a3d --- /dev/null +++ b/.github/workflows/build-and-push-to-docker-hub.yml @@ -0,0 +1,37 @@ +name: Build and push image to Docker Hub + +on: + push: + branches: + - master + +jobs: + build-and-push-image-to-docker-hub: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + - name: Authenticate to Docker Hub + uses: docker/login-action@v3 + with: + registry: docker.io + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PAT }} + + - name: Get commit SHA # This retrieves a shortened version of the commit SHA and stores is as an environment variable + run: echo "COMMIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + + - name: Build and push image to Docker hub + uses: docker/build-push-action@v2 + with: + context: . + file: ./Dockerfile + sbom: true # Enable SBOM + push: true + tags: | # Second tag makes it easy to pull the image to deployment environment + ${{ secrets.DOCKER_USERNAME }}/chat-room:${{env.COMMIT_SHA}} + ${{secrets.DOCKER_USERNAME}}/chat-room:latest \ No newline at end of file diff --git a/.github/workflows/container-scan.yml b/.github/workflows/container-scan.yml new file mode 100644 index 000000000..373075f6c --- /dev/null +++ b/.github/workflows/container-scan.yml @@ -0,0 +1,38 @@ +name: Docker scout CVE scan + +on: + pull_request: + branches: + - master + +jobs: + scan-docker-image-for-vulnerabilities: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + - name: Authenticate to Docker hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PAT }} + + - name: Build image (Dirty way to do scout) + run: docker build -t chat-room . + + - name: Check for CVEs on Docker scout + uses: docker/scout-action@v1 + with: + command: cves + image: chat-room:latest + ignore-unchanged: true + only-severities: critical,high + + - name: Provide recommendations + uses: docker/scout-action@v1 + with: + command: recommendations \ No newline at end of file diff --git a/.github/workflows/shell_scripts/decrypt_service_account.sh b/.github/workflows/shell_scripts/decrypt_service_account.sh new file mode 100644 index 000000000..8110e0bb6 --- /dev/null +++ b/.github/workflows/shell_scripts/decrypt_service_account.sh @@ -0,0 +1,4 @@ +#!/bin/sh +file_name=${GITHUB_WORKSPACE}/terraform/key.json.gpg + +gpg --quiet --batch --yes --decrypt --passphrase="$GCP_SERVICE_ACCOUNT_PASSPHRASE" --output ${GITHUB_WORKSPACE}/terraform/key.json $file_name \ No newline at end of file diff --git a/.github/workflows/snyk-security.yml b/.github/workflows/snyk-security.yml new file mode 100644 index 000000000..08ccb1a06 --- /dev/null +++ b/.github/workflows/snyk-security.yml @@ -0,0 +1,80 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# A sample workflow which sets up Snyk to analyze the full Snyk platform (Snyk Open Source, Snyk Code, +# Snyk Container and Snyk Infrastructure as Code) +# The setup installs the Snyk CLI - for more details on the possible commands +# check https://docs.snyk.io/snyk-cli/cli-reference +# The results of Snyk Code are then uploaded to GitHub Security Code Scanning +# +# In order to use the Snyk Action you will need to have a Snyk API token. +# More details in https://github.com/snyk/actions#getting-your-snyk-token +# or you can signup for free at https://snyk.io/login +# +# For more examples, including how to limit scans to only high-severity issues +# and fail PR checks, see https://github.com/snyk/actions/ + +name: Snyk Security + +on: + pull_request: + branches: ["master"] + +permissions: + contents: read + +jobs: + snyk: + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Snyk CLI to check for security issues + # Snyk can be used to break the build when it detects security issues. + # In this case we want to upload the SAST issues to GitHub Code Scanning + uses: snyk/actions/setup@806182742461562b67788a64410098c9d9b96adb + + # For Snyk Open Source, you must first set up the development environment for your application's dependencies + # For example for Node + #- uses: actions/setup-node@v3 + # with: + # node-version: 16 + - uses: actions/setup-python@v2 + with: + python-version: 3.11 + + + + env: + # This is where you will need to introduce the Snyk API token created with your Snyk account + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + - name: Authenticate with Snyk + run: snyk auth ${{ secrets.SNYK_TOKEN }} + + # Runs Snyk Code (SAST) analysis and uploads result into GitHub. + # Use || true to not fail the pipeline + - name: Snyk Code test + run: snyk code test --sarif > snyk-code.sarif || true + + - name: Get python dependencies + run: pip install -r requirements.txt + + # Runs Snyk Open Source (SCA) analysis and uploads result to Snyk. + - name: Snyk Open Source monitor + run: snyk monitor --all-projects + + # Runs Snyk Infrastructure as Code (IaC) analysis and uploads result to Snyk. + # Use || true to not fail the pipeline. + # - name: Snyk IaC test and report + # run: snyk iac test --report || true + + # Push the Snyk Code results into GitHub Code Scanning tab + - name: Upload result to GitHub Code Scanning + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: snyk-code.sarif diff --git a/.github/workflows/test-gcp-infra-setup.yml b/.github/workflows/test-gcp-infra-setup.yml new file mode 100644 index 000000000..7aaf313f0 --- /dev/null +++ b/.github/workflows/test-gcp-infra-setup.yml @@ -0,0 +1,47 @@ +name: GCP tests + +on: + pull_request: + branches: + - master + +jobs: + run-gcp-tests: + runs-on: ubuntu-latest + + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Terraform + uses: hashicorp/setup-terraform@v1 + with: + terraform_version: 1.7.5 + + - name: Initialize Terraform + env: + AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }} + AWS_SECRET_KEY: ${{ secrets.AWS_SECRET_KEY }} + run: terraform -chdir=${GITHUB_WORKSPACE}/terraform init -backend-config="access_key=$AWS_ACCESS_KEY" -backend-config="secret_key=$AWS_SECRET_KEY" + + - name: Check formatting + run: terraform -chdir=${GITHUB_WORKSPACE}/terraform fmt -check + + - name: Check for bugs + run: terraform -chdir=${GITHUB_WORKSPACE}/terraform validate + + - name: Decrypt service account key + env: + GCP_SERVICE_ACCOUNT_PASSPHRASE: ${{ secrets.GCP_SERVICE_ACCOUNT_PASSPHRASE }} + run: | + chmod +x ${GITHUB_WORKSPACE}/.github/workflows/shell_scripts/decrypt_service_account.sh + bash ${GITHUB_WORKSPACE}/.github/workflows/shell_scripts/decrypt_service_account.sh + + - name: Does your configuration make sense? + env: + GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} + run: | + terraform -chdir=${GITHUB_WORKSPACE}/terraform plan \ + -var path_to_gcp_service_account="${GITHUB_WORKSPACE}/terraform/key.json" \ + -var project_id=$GCP_PROJECT_ID \ No newline at end of file diff --git a/.github/workflows/zap-baseline-scan.yml b/.github/workflows/zap-baseline-scan.yml new file mode 100644 index 000000000..aedebb7ab --- /dev/null +++ b/.github/workflows/zap-baseline-scan.yml @@ -0,0 +1,24 @@ +name: Perform ZAP baseline scan (Dynamic Application Security Testing) + +on: + pull_request: + branches: ["master"] +jobs: + perform-baseline-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build docker image + run: docker build -t chat-room:latest . + + - name: Run docker image + run: docker run -dp 8000:8000 chat-room:latest + + - name: Set up OWASP ZAP baseline scan + uses: zaproxy/action-baseline@v0.12.0 + with: + target: 'http://localhost:8000' + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..39a6482fa --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +chat-room-env/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..9a38dba73 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: + - repo: https://github.com/gitleaks/gitleaks + rev: v8.22.0 + hooks: + - id: gitleaks \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..1ef33795b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +# syntax=docker/dockerfile:1 + +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Dockerfile reference guide at +# https://docs.docker.com/go/dockerfile-reference/ + +ARG PYTHON_VERSION=3.12 +FROM python:${PYTHON_VERSION}-slim-bookworm AS base + +# Prevents Python from writing pyc files. +ENV PYTHONDONTWRITEBYTECODE=1 + +# Keeps Python from buffering stdout and stderr to avoid situations where +# the application crashes without emitting any logs due to buffering. +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +# Create a non-privileged user that the app will run under. +# See https://docs.docker.com/go/dockerfile-user-best-practices/ +ARG UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + appuser + +# Download dependencies as a separate step to take advantage of Docker's caching. +# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. +# Leverage a bind mount to requirements.txt to avoid having to copy them into +# into this layer. +RUN --mount=type=cache,target=/root/.cache/pip \ + --mount=type=bind,source=requirements.txt,target=requirements.txt \ + python -m pip install -r requirements.txt + +# Switch to the non-privileged user to run the application. +USER appuser + +# Copy the source code into the container. +COPY . . + +# Expose the port that the application listens on. +EXPOSE 8001 + +# Run the application. +CMD python manage.py runserver 0.0.0.0:8000 diff --git a/README.Docker.md b/README.Docker.md new file mode 100644 index 000000000..6dae561fa --- /dev/null +++ b/README.Docker.md @@ -0,0 +1,22 @@ +### Building and running your application + +When you're ready, start your application by running: +`docker compose up --build`. + +Your application will be available at http://localhost:8000. + +### Deploying your application to the cloud + +First, build your image, e.g.: `docker build -t myapp .`. +If your cloud uses a different CPU architecture than your development +machine (e.g., you are on a Mac M1 and your cloud provider is amd64), +you'll want to build the image for that platform, e.g.: +`docker build --platform=linux/amd64 -t myapp .`. + +Then, push it to your registry, e.g. `docker push myregistry.com/myapp`. + +Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/) +docs for more detail on building and pushing. + +### References +* [Docker's Python guide](https://docs.docker.com/language/python/) \ No newline at end of file diff --git a/README.md b/README.md index b1f339483..b6bad5054 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,18 @@ pip install -r requirements.txt ``` -# +### Running the App on Docker +--> Run the application using the following command: +```bash +docker compose up -d --build + +``` +Navigate to the application on `localhost:8000` + +If you have traefik ensure that traefik is running first then visit https://chatroom.docker.localhost + -### Running the App +### Running the App on PC --> To run the App, we use : ```bash diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 000000000..0d9e14af1 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,59 @@ +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Docker Compose reference guide at +# https://docs.docker.com/go/compose-spec-reference/ + +# Here the instructions define your application as a service called "server". +# This service is built from the Dockerfile in the current directory. +# You can add other services your application may depend on here, such as a +# database or a cache. For examples, see the Awesome Compose repository: +# https://github.com/docker/awesome-compose +services: + chat-room: + build: + context: . + container_name: chat-room + restart: unless-stopped + networks: + - traefik + labels: + - "traefik.enable=true" + - "traefik.http.routers.django.rule=Host(`chatroom.docker.localhost`)" + - "traefik.http.services.django.loadbalancer.server.port=8000" + - "traefik.http.routers.django.entrypoints=websecure" +networks: + traefik: + external: true + +# The commented out section below is an example of how to define a PostgreSQL +# database that your application can use. `depends_on` tells Docker Compose to +# start the database before your application. The `db-data` volume persists the +# database data between container restarts. The `db-password` secret is used +# to set the database password. You must create `db/password.txt` and add +# a password of your choosing to it before running `docker compose up`. +# depends_on: +# db: +# condition: service_healthy +# db: +# image: postgres +# restart: always +# user: postgres +# secrets: +# - db-password +# volumes: +# - db-data:/var/lib/postgresql/data +# environment: +# - POSTGRES_DB=example +# - POSTGRES_PASSWORD_FILE=/run/secrets/db-password +# expose: +# - 5432 +# healthcheck: +# test: [ "CMD", "pg_isready" ] +# interval: 10s +# timeout: 5s +# retries: 5 +# volumes: +# db-data: +# secrets: +# db-password: +# file: db/password.txt + diff --git a/requirements.txt b/requirements.txt index a69f01974..9cc6c02bd 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/studybud/settings.py b/studybud/settings.py index ec12aa7c0..bf17b48ec 100644 --- a/studybud/settings.py +++ b/studybud/settings.py @@ -25,7 +25,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ["0.0.0.0", "127.0.0.1", "chatroom.kiirumaina.com", "chatroom.docker.localhost"] # Application definition diff --git a/terraform/backend.tf b/terraform/backend.tf new file mode 100644 index 000000000..b16ad5758 --- /dev/null +++ b/terraform/backend.tf @@ -0,0 +1,9 @@ +terraform { + + backend "s3" { + bucket = "terraform-state-bucket-270420241723" + key = "global/gcp/terraform.tfstate" + region = "us-east-2" + encrypt = true + } +} \ No newline at end of file diff --git a/terraform/key.json.gpg b/terraform/key.json.gpg new file mode 100644 index 000000000..e0e109240 Binary files /dev/null and b/terraform/key.json.gpg differ diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 000000000..30b16b4b9 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,67 @@ +terraform { + required_providers { + google = { + source = "hashicorp/google" + version = "5.28.0" + } + } +} + +provider "google" { + project = var.project_id + region = var.region + credentials = file(var.path_to_gcp_service_account) +} + +resource "google_cloud_run_v2_service" "chat-room-deployment" { + name = "chat-room-deployment" + location = var.region + ingress = "INGRESS_TRAFFIC_ALL" + template { + containers { + image = "docker.io/kiiru4reals/chat-room:latest" + ports { + name = "http1" + container_port = 8000 + } + resources { + limits = { + cpu = "1" + memory = "512Mi" + } + startup_cpu_boost = false + cpu_idle = false + } + } + scaling { + min_instance_count = 0 + max_instance_count = 1 + + } + timeout = "60.0s" + max_instance_request_concurrency = 5 + execution_environment = "EXECUTION_ENVIRONMENT_GEN1" + } +} + +resource "google_cloud_run_v2_service_iam_member" "allow-public-access" { + location = google_cloud_run_v2_service.chat-room-deployment.location + name = google_cloud_run_v2_service.chat-room-deployment.name + role = "roles/run.invoker" + member = "allUsers" + +} + +# Set up domain mapping manually +# data "google_project" "project_info" {} + +# resource "google_" "chatroom-kiiru-maina-com" { +# name = "chatroom.kiirumaina.com" +# location = google_cloud_run_v2_service.chat-room-deployment.location +# metadata { +# namespace = data.google_project.project_info.project_id +# } +# spec { +# route_name = google_cloud_run_v2_service.chat-room-deployment.name +# } +# } \ No newline at end of file diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 000000000..c14f778ad --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,15 @@ +variable "project_id" { + description = "Project ID for GCP project" + type = string +} + +variable "region" { + description = "Region for GCP project" + type = string + default = "us-central1" +} + +variable "path_to_gcp_service_account" { + description = "Path to the GCP service account credentials" + type = string +} \ No newline at end of file