diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7494ff97b..721031063 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI +name: CI Pipeline on: push: @@ -7,35 +7,99 @@ on: - 'k8s/**' - 'docs/**' - '**/*.md' + - terraform/** + workflow_dispatch: + +permissions: + id-token: write + contents: read + +env: + REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com jobs: - build-and-push: + security: + name: Security Gate runs-on: ubuntu-latest + steps: - uses: actions/checkout@v4 - - uses: docker/setup-buildx-action@v4 + - name: Gitleaks + uses: gitleaks/gitleaks-action@v2 - - uses: docker/login-action@v4 - if: ${{ vars.DEPLOY_ENABLED == 'true' }} + - name: Hadolint Backend + uses: hadolint/hadolint-action@v3.1.0 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + dockerfile: backend/Dockerfile - - name: Build and push backend - uses: docker/build-push-action@v7 + - name: Hadolint Frontend + uses: hadolint/hadolint-action@v3.1.0 with: - context: ./backend - push: ${{ vars.DEPLOY_ENABLED == 'true' }} - tags: | - ${{ secrets.DOCKERHUB_USERNAME != '' && format('{0}/skillpulse-backend:latest', secrets.DOCKERHUB_USERNAME) || 'skillpulse-backend:latest' }} - ${{ secrets.DOCKERHUB_USERNAME != '' && format('{0}/skillpulse-backend:{1}', secrets.DOCKERHUB_USERNAME, github.sha) || format('skillpulse-backend:{0}', github.sha) }} + dockerfile: frontend/Dockerfile - - name: Build and push frontend - uses: docker/build-push-action@v7 + - name: Setup Go + uses: actions/setup-go@v5 with: - context: ./frontend - push: ${{ vars.DEPLOY_ENABLED == 'true' }} + go-version-file: backend/go.mod + cache: true + + - name: Run govulncheck + run: | + cd backend + go install golang.org/x/vuln/cmd/govulncheck@latest + govulncheck ./... + + build-scan-push: + name: Build, Scan & Push + runs-on: ubuntu-latest + needs: security + + strategy: + fail-fast: false + matrix: + service: [backend, frontend] + + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Set TAG + run: echo "TAG=${GITHUB_SHA:0:7}" >> $GITHUB_ENV + + - name: Configure AWS (OIDC) + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Login to ECR + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build image + uses: docker/build-push-action@v6 + with: + context: ./${{ matrix.service }} + load: true + push: false tags: | - ${{ secrets.DOCKERHUB_USERNAME != '' && format('{0}/skillpulse-frontend:latest', secrets.DOCKERHUB_USERNAME) || 'skillpulse-frontend:latest' }} - ${{ secrets.DOCKERHUB_USERNAME != '' && format('{0}/skillpulse-frontend:{1}', secrets.DOCKERHUB_USERNAME, github.sha) || format('skillpulse-frontend:{0}', github.sha) }} + ${{ env.REGISTRY }}/skillpulse-${{ matrix.service }}:${{ env.TAG }} + ${{ env.REGISTRY }}/skillpulse-${{ matrix.service }}:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Trivy Scan + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ env.REGISTRY }}/skillpulse-${{ matrix.service }}:${{ env.TAG }} + format: table + severity: CRITICAL,HIGH + exit-code: 1 + ignore-unfixed: true + + - name: Push Images + run: | + IMAGE=$REGISTRY/skillpulse-${{ matrix.service }} + docker push $IMAGE:$TAG + docker push $IMAGE:latest diff --git a/.github/workflows/code-quality-check.yml b/.github/workflows/code-quality-check.yml new file mode 100644 index 000000000..f791c8ffb --- /dev/null +++ b/.github/workflows/code-quality-check.yml @@ -0,0 +1,59 @@ +name: Go lint + +on: + push: + branches: [main,feat/dev] + workflow_dispatch: + #pull_request: + #branches:[feat/dev] + # +jobs: + validate: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + go-version: ["1.26"] + defaults: + run: + working-directory: ./backend + steps: + - name: checkout code + uses: actions/checkout@v4 + + - name: setup Go ${{ matrix.go-version}} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version}} + cache: true + cache-dependency-path: backend/go.sum + + - name: verify dependencies + run: go mod verify + + - name: download dependencies + run: go mod download + + - name: run go fmt check + run: | + if [ -n "$(gofmt -l .)" ]; then + echo "Go files must be formatted with gofmt" + gofmt -l . + exit 1 + fi + + - name: run go vet + run: go vet ./... + + - name: run staticcheck + run: | + go install honnef.co/go/tools/cmd/staticcheck@latest + staticcheck ./... + + - name: run golangci-lint + # Skip golangci-lint for Go 1.26 due to compatibility issues + if: matrix.go-version != '1.26' + uses: golangci/golangci-lint-action@v4 + with: + version: latest + working-directory: backend diff --git a/.github/workflows/dependency-scan.yml b/.github/workflows/dependency-scan.yml new file mode 100644 index 000000000..edf0407da --- /dev/null +++ b/.github/workflows/dependency-scan.yml @@ -0,0 +1,97 @@ +name: Dependency Vulnerability Scan + +on: + workflow_call: + +jobs: + dependency-scan: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + + matrix: + go-version: ["1.21", "1.22", "1.23"] + + steps: + + # ============================== + # Checkout Repository + # ============================== + + - name: Checkout Code + uses: actions/checkout@v4 + + # ============================== + # Setup Go Environment + # ============================== + + - name: Setup Go ${{ matrix.go-version }} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + + # ============================== + # Verify Go Installation + # ============================== + + - name: Verify Go Version + run: go version + + # ============================== + # Verify go.mod Exists + # ============================== + + - name: Check go.mod + run: | + if [ ! -f go.mod ]; then + echo "go.mod file not found!" + exit 1 + fi + + # ============================== + # Install Dependencies + # ============================== + + - name: Install Dependencies + run: | + go mod tidy + go mod download + + # ============================== + # Run govulncheck + # ============================== + + - name: Install govulncheck + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + + - name: Run Dependency Vulnerability Scan + run: | + govulncheck ./... + + # ============================== + # Run Trivy Filesystem Scan + # ============================== + + - name: Run Trivy Filesystem Scan + uses: aquasecurity/trivy-action@0.24.0 + with: + scan-type: fs + scan-ref: . + format: table + exit-code: 1 + ignore-unfixed: true + severity: CRITICAL,HIGH + + # ============================== + # Upload Trivy Report (Optional) + # ============================== + + - name: Upload Trivy Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: trivy-dependency-report + path: trivy-results.sarif + if-no-files-found: ignore diff --git a/.github/workflows/deploy-to-server.yml b/.github/workflows/deploy-to-server.yml new file mode 100644 index 000000000..ed9f3186b --- /dev/null +++ b/.github/workflows/deploy-to-server.yml @@ -0,0 +1,52 @@ +# this will deploy the safe / secure / tested app to Prod server +name: Deploy To Server + +on: + workflow_call: + + +jobs: + deploy: + runs-on: ubuntu-latest + env: + DOCKERHUB_USER: ${{ vars.DOCKERHUB_USER }} + DOCKER_TAG: ${{ github.sha }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: SSH to Prod server & create app folder + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.EC2_SSH_HOST }} + username: ${{ secrets.EC2_SSH_USER }} + key: ${{ secrets.EC2_SSH_PRIVATE_KEY }} + script: | + echo "Installing required packages & folders" + sudo apt-get update && sudo apt-get install docker.io docker-compose-v2 -y + sudo usermod -aG docker $USER + mkdir -p devops + + - name: Copy the docker-compose file + uses: appleboy/scp-action@v1 + with: + host: ${{ secrets.EC2_SSH_HOST }} + username: ${{ secrets.EC2_SSH_USER }} + key: ${{ secrets.EC2_SSH_PRIVATE_KEY }} + source: docker-compose.yml + target: ~/devops + + - name: SSH to Prod server & run the app + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.EC2_SSH_HOST }} + username: ${{ secrets.EC2_SSH_USER }} + key: ${{ secrets.EC2_SSH_PRIVATE_KEY }} + script: | + echo "Running App" + + export DOCKERHUB_USER=${{ vars.DOCKERHUB_USER }} + export DOCKER_TAG=${{ github.sha }} + echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login --username ${{ vars.DOCKERHUB_USER }} --password-stdin + cd ~/devops && docker compose down && docker compose up -d --build --force-recreate --pull always diff --git a/.github/workflows/devsecops-pipeline.yml b/.github/workflows/devsecops-pipeline.yml new file mode 100644 index 000000000..2ed0f0907 --- /dev/null +++ b/.github/workflows/devsecops-pipeline.yml @@ -0,0 +1,46 @@ +name: DevSecOps End To End Pipeline + +on: + push: + branches: + - main + - feat/dev + +jobs: + # CI (Continous Integration with Security Scanning) + code-quality: + uses: ./.github/workflows/code-quality-check.yml + + secrets-scan: + uses: ./.github/workflows/secret-scan.yml + secrets: inherit + + dependency-scan: + uses: ./.github/workflows/dependency-scan.yml + + docker-scan: + uses: ./.github/workflows/docker-lint.yml + + # Build once the security scans and tests are complete + build: + needs: [code-quality, secrets-scan, dependency-scan, docker-scan] + uses: ./.github/workflows/docker-build-push.yml + secrets: inherit + + tests: + uses: ./.github/workflows/tests.yml + + # Image scan + trivy: + needs: [build] + uses: ./.github/workflows/image-scan.yml + secrets: inherit + + + # CD : Deploy to Production + deploy: + needs: [trivy] + uses: ./.github/workflows/deploy-to-server.yml + secrets: inherit + + diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml new file mode 100644 index 000000000..b79234677 --- /dev/null +++ b/.github/workflows/docker-build-push.yml @@ -0,0 +1,29 @@ +name: Docker Build & Push + +on: + workflow_call: + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + + - name: Code Checkout + uses: actions/checkout@v4 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build & Push to Docker Hub + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + ${{ vars.DOCKERHUB_USER }}/github-actions-app:${{ github.ref_name }} + ${{ vars.DOCKERHUB_USER }}/github-actions-app:latest + ${{ vars.DOCKERHUB_USER }}/github-actions-app:${{ github.sha}} diff --git a/.github/workflows/docker-lint.yml b/.github/workflows/docker-lint.yml new file mode 100644 index 000000000..e0873d4d1 --- /dev/null +++ b/.github/workflows/docker-lint.yml @@ -0,0 +1,25 @@ +name: Docker Lint + +on: + workflow_call: + pull_request: + +jobs: + docker-lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Lint Backend Dockerfile + run: | + docker run --rm \ + -v ${{ github.workspace }}/backend/Dockerfile:/Dockerfile \ + hadolint/hadolint hadolint --ignore DL3018 /Dockerfile + + - name: Lint Frontend Dockerfile + run: | + docker run --rm \ + -v ${{ github.workspace }}/frontend/Dockerfile:/Dockerfile \ + hadolint/hadolint hadolint --ignore DL3018 /Dockerfile diff --git a/.github/workflows/image-scan.yml b/.github/workflows/image-scan.yml new file mode 100644 index 000000000..bb3501af2 --- /dev/null +++ b/.github/workflows/image-scan.yml @@ -0,0 +1,47 @@ +name: Image scanner + +on: + workflow_call: + secrets: + DOCKER_USERNAME: + required: true + DOCKER_PASSWORD: + required: true + +jobs: + image-scanner: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + # FIXED: Swapped out mismatched names to match the inputs declared above exactly + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + # Note: Removed the manual Trivy installation apt steps since the action below handles it natively + + - name: Scan Backend Image + uses: aquasecurity/trivy-action@v0.36.0 # Fixed the version prefix to prevent resolution errors + with: + image-ref: ${{ secrets.DOCKER_USERNAME }}/skillpulse-backend:latest + format: 'table' + exit-code: 1 + severity: 'CRITICAL,HIGH' + trivyignores: .trivyignore + + - name: Scan Frontend Image + uses: aquasecurity/trivy-action@v0.36.0 # Fixed all version prefix to prevent resolution errors + with: + image-ref: ${{ secrets.DOCKER_USERNAME }}/skillpulse-frontend:latest + format: 'table' + exit-code: 1 + severity: 'CRITICAL,HIGH' + + - name: Logout from Docker Hub + if: always() + run: docker logout diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml new file mode 100644 index 000000000..3a8ed9d9f --- /dev/null +++ b/.github/workflows/secret-scan.yml @@ -0,0 +1,19 @@ +name: Secret scan + +on: + workflow_call: + + +jobs: + secret-scan: + runs-on: ubuntu-latest + steps: + - name: checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: run gitleaks + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # github action manage it diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..c5587c80a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,102 @@ +name: Go Unit Tests + +on: + workflow_call: + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + + matrix: + go-version: ["1.21", "1.22", "1.23"] + + steps: + + # ============================== + # Checkout Repository + # ============================== + + - name: Checkout Code + uses: actions/checkout@v4 + + # ============================== + # Setup Go + # ============================== + + - name: Setup Go ${{ matrix.go-version }} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + + # ============================== + # Verify Go Installation + # ============================== + + - name: Verify Go Version + run: go version + + # ============================== + # Verify go.mod Exists + # ============================== + + - name: Check go.mod + run: | + if [ ! -f go.mod ]; then + echo "go.mod file not found!" + exit 1 + fi + + # ============================== + # Cache Go Modules + # ============================== + + - name: Cache Go Modules + uses: actions/cache@v4 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + + restore-keys: | + ${{ runner.os }}-go- + + # ============================== + # Install Dependencies + # ============================== + + - name: Install Dependencies + run: | + go mod tidy + go mod download + + # ============================== + # Run Unit Tests + # ============================== + + - name: Run Go Tests + run: | + go test ./... -v + + # ============================== + # Generate Coverage Report + # ============================== + + - name: Generate Coverage Report + run: | + go test ./... -coverprofile=coverage.out + + # ============================== + # Upload Coverage Report + # ============================== + + - name: Upload Coverage Report + uses: actions/upload-artifact@v4 + with: + name: go-coverage-report + path: coverage.out + if-no-files-found: ignore diff --git a/Ansible/playbook.yml b/Ansible/playbook.yml new file mode 100644 index 000000000..d56ac6858 --- /dev/null +++ b/Ansible/playbook.yml @@ -0,0 +1,46 @@ +--- +- name: Configure SkillPulse EC2 Instance + hosts: all + become: yes + tasks: + - name: Update apt packages + apt: + update_cache: yes + cache_valid_time: 3600 + + - name: Install dependencies + apt: + name: + - apt-transport-https + - ca-certificates + - curl + - software-properties-common + - git + state: present + + - name: Add Docker GPG key + apt_key: + url: https://download.docker.com/linux/ubuntu/gpg + state: present + + - name: Add Docker repository + apt_repository: + repo: deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable + state: present + + - name: Install Docker + apt: + name: docker-ce + state: present + + - name: Ensure Docker is running and enabled + service: + name: docker + state: started + enabled: yes + + - name: Add user to docker group + user: + name: ubuntu + groups: docker + append: yes diff --git a/backend/go.mod b/backend/go.mod index 65c447eb0..0ab5eece0 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,4 +1,4 @@ -module github.com/trainwithshubham/skillpulse +module github.com/manishvishwakarma89/skillpulse go 1.26 diff --git a/backend/main.go b/backend/main.go index 132d1741b..e1f2a8ce8 100644 --- a/backend/main.go +++ b/backend/main.go @@ -5,8 +5,8 @@ import ( "os" "github.com/gin-gonic/gin" - "github.com/trainwithshubham/skillpulse/database" - "github.com/trainwithshubham/skillpulse/handlers" + "github.com/manishvishwakarma89/skillpulse/database" + "github.com/manishvishwakarma89/skillpulse/handlers" ) func main() { diff --git a/terraform-iac/outputs.tf b/terraform-iac/outputs.tf new file mode 100644 index 000000000..47177120f --- /dev/null +++ b/terraform-iac/outputs.tf @@ -0,0 +1,7 @@ +output "instance_public_ip" { + value = aws_instance.my_ec2.public_ip +} + +output "instance_id" { + value = aws_instance.my_ec2.id +}