diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml index 81b85b71..25e81e4f 100644 --- a/.github/workflows/branch.yml +++ b/.github/workflows/branch.yml @@ -9,18 +9,26 @@ jobs: lint-fmt: name: Lint & Fmt runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Checkout uses: actions/checkout@v4 - + with: + persist-credentials: false + - name: Setup Go uses: actions/setup-go@v5 with: go-version: 1.25.3 + # TODO: Pin this action to a commit SHA for security + # Visit https://github.com/go-task/setup-task/releases to find the real hash for v1 - name: Install Task uses: go-task/setup-task@v1 + # TODO: Pin this action to a commit SHA for security + # Visit https://github.com/golangci/golangci-lint-action/releases to find the real hash for v9 - name: Install golangci-lint uses: golangci/golangci-lint-action@v9 with: @@ -38,21 +46,29 @@ jobs: unittest: name: Unit Tests runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Checkout uses: actions/checkout@v4 - + with: + persist-credentials: false + - name: Setup Go uses: actions/setup-go@v5 with: go-version: 1.25.3 + # TODO: Pin this action to a commit SHA for security + # Visit https://github.com/go-task/setup-task/releases to find the real hash for v1 - name: Install Task uses: go-task/setup-task@v1 - name: Install mockgen run: task install-mockgen + # TODO: Pin this action to a commit SHA for security + # Visit https://github.com/dagger/dagger-for-github/releases to find the real hash for v8.2.0 - name: Generate main dagger code uses: dagger/dagger-for-github@v8.2.0 with: @@ -60,6 +76,8 @@ jobs: args: --sdk=go --compat=skip version: "v0.19.6" + # TODO: Pin this action to a commit SHA for security + # Visit https://github.com/dagger/dagger-for-github/releases to find the real hash for v8.2.0 - name: Generate workspace dagger code uses: dagger/dagger-for-github@v8.2.0 with: @@ -77,10 +95,14 @@ jobs: security: name: Security checks runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Checkout uses: actions/checkout@v4 - + with: + persist-credentials: false + - name: Setup Go uses: actions/setup-go@v5 with: @@ -90,12 +112,16 @@ jobs: - name: Install gosec run: go install github.com/securego/gosec/v2/cmd/gosec@latest + # TODO: Pin this action to a commit SHA for security + # Visit https://github.com/go-task/setup-task/releases to find the real hash for v1 - name: Install Task uses: go-task/setup-task@v1 - name: Install mockgen run: task install-mockgen + # TODO: Pin this action to a commit SHA for security + # Visit https://github.com/dagger/dagger-for-github/releases to find the real hash for v8.2.0 - name: Generate main dagger code uses: dagger/dagger-for-github@v8.2.0 with: @@ -103,6 +129,8 @@ jobs: args: --sdk=go --compat=skip version: "v0.19.6" + # TODO: Pin this action to a commit SHA for security + # Visit https://github.com/dagger/dagger-for-github/releases to find the real hash for v8.2.0 - name: Generate workspace dagger code uses: dagger/dagger-for-github@v8.2.0 with: diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 2b28443b..b4023362 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -22,6 +22,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Authenticate to Google Cloud uses: google-github-actions/auth@v2 @@ -73,6 +75,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Authenticate to Google Cloud uses: google-github-actions/auth@v2 @@ -94,7 +98,7 @@ jobs: timeout-minutes: 10 run: | terraform plan \ - -var="base_url=${{ vars.BASE_URL }}" \ + -var="base_url=${VARS_BASE_URL}" \ -var="project_id=${{ env.PROJECT_ID }}" \ -var="region=${{ env.REGION }}" \ -var="gh_client_id=${{ secrets.GH_CLIENT_ID }}" \ @@ -105,14 +109,18 @@ jobs: -var="stripe_publishable_key=${{ secrets.STRIPE_PUBLISHABLE }}" \ -var="stripe_webhook_secret=${{ secrets.STRIPE_WEBHOOK_SECRET }}" \ -var="openai_api_key=${{ secrets.OPENAI_API_KEY }}" \ + -var="resend_api_key=${{ secrets.RESEND_API_KEY }}" \ + -var="feedback_to_email=${{ vars.FEEDBACK_TO_EMAIL || 'info@notifications.scalabit.dev' }}" \ -var="container_image=${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/workflow-scanner/workflow-scanner:${{ github.sha }}" + env: + VARS_BASE_URL: ${{ vars.BASE_URL }} - name: Terraform Apply (Production) working-directory: ./terraform/environments/production timeout-minutes: 15 run: | terraform apply -auto-approve \ - -var="base_url=${{ vars.BASE_URL }}" \ + -var="base_url=${VARS_BASE_URL}" \ -var="project_id=${{ env.PROJECT_ID }}" \ -var="region=${{ env.REGION }}" \ -var="gh_client_id=${{ secrets.GH_CLIENT_ID }}" \ @@ -123,7 +131,11 @@ jobs: -var="stripe_publishable_key=${{ secrets.STRIPE_PUBLISHABLE }}" \ -var="stripe_webhook_secret=${{ secrets.STRIPE_WEBHOOK_SECRET }}" \ -var="openai_api_key=${{ secrets.OPENAI_API_KEY }}" \ + -var="resend_api_key=${{ secrets.RESEND_API_KEY }}" \ + -var="feedback_to_email=${{ vars.FEEDBACK_TO_EMAIL || 'info@notifications.scalabit.dev' }}" \ -var="container_image=${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/workflow-scanner/workflow-scanner:${{ github.sha }}" + env: + VARS_BASE_URL: ${{ vars.BASE_URL }} - name: Cleanup Terraform lock on failure if: failure() diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aa07229e..cc81c81a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,6 +17,7 @@ jobs: with: ref: ${{ github.event.pull_request.merge_commit_sha }} fetch-depth: '0' + persist-credentials: false - name: Output semver id: get_bump @@ -32,4 +33,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DEFAULT_BUMP: ${{ steps.get_bump.outputs.bump }} - TAG_PREFIX: v \ No newline at end of file + TAG_PREFIX: v diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c902db3b..4aa9dc0a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -16,6 +16,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v5 + with: + persist-credentials: false - name: Set up Go uses: actions/setup-go@v6 @@ -48,7 +50,7 @@ jobs: # Also tag with specific version if this is a release if [[ "${{ github.event_name }}" == "release" ]]; then - docker tag mariominhava04/workflow-scanner:latest mariominhava04/workflow-scanner:${{ env.TAG }} - docker push mariominhava04/workflow-scanner:${{ env.TAG }} + docker tag mariominhava04/workflow-scanner:latest mariominhava04/workflow-scanner:${TAG} + docker push mariominhava04/workflow-scanner:${TAG} fi diff --git a/.github/workflows/sandbox-deploy.yml b/.github/workflows/sandbox-deploy.yml index 293b60fe..cb5b07c5 100644 --- a/.github/workflows/sandbox-deploy.yml +++ b/.github/workflows/sandbox-deploy.yml @@ -30,6 +30,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Authenticate to Google Cloud uses: google-github-actions/auth@v2 @@ -70,6 +72,8 @@ jobs: -var="stripe_publishable_key=${{ secrets.TEST_STRIPE_PK }}" \ -var="stripe_webhook_secret=${{ secrets.TEST_STRIPE_WEBHOOK_SECRET }}" \ -var="openai_api_key=${{ secrets.OPENAI_API_KEY }}" \ + -var="resend_api_key=${{ secrets.RESEND_API_KEY }}" \ + -var="feedback_to_email=${{ vars.FEEDBACK_TO_EMAIL || 'info@notifications.scalabit.dev' }}" \ -var="sandbox_allowed_users=${{ secrets.SANDBOX_ALLOWED_USERS }}" - name: Configure Docker to use gcloud as credential helper @@ -99,6 +103,8 @@ jobs: -var="stripe_publishable_key=${{ secrets.TEST_STRIPE_PK }}" \ -var="stripe_webhook_secret=${{ secrets.TEST_STRIPE_WEBHOOK_SECRET }}" \ -var="openai_api_key=${{ secrets.OPENAI_API_KEY }}" \ + -var="resend_api_key=${{ secrets.RESEND_API_KEY }}" \ + -var="feedback_to_email=${{ vars.FEEDBACK_TO_EMAIL || 'info@notifications.scalabit.dev' }}" \ -var="sandbox_allowed_users=${{ secrets.SANDBOX_ALLOWED_USERS }}" - name: Get Service URL @@ -108,4 +114,4 @@ jobs: --region=${{ env.REGION }} \ --format='value(status.url)') echo "Sandbox deployed to: $SERVICE_URL" - echo "::notice title=Sandbox Deployment::Sandbox environment deployed to $SERVICE_URL" \ No newline at end of file + echo "::notice title=Sandbox Deployment::Sandbox environment deployed to $SERVICE_URL" diff --git a/.github/workflows/vulnerable-workflow.yml b/.github/workflows/vulnerable-workflow.yml index e12fb50a..c6e10792 100644 --- a/.github/workflows/vulnerable-workflow.yml +++ b/.github/workflows/vulnerable-workflow.yml @@ -1,27 +1,36 @@ -name: Vulnerable Workflow for Testing +name: Vulnerable Workflow on: - workflow_dispatch: + pull_request: + branches: + - main jobs: test: runs-on: ubuntu-latest - + permissions: + contents: read + steps: - name: Checkout uses: actions/checkout@v2 - + with: + persist-credentials: false + - name: Print PR title (Script Injection Vulnerability) run: | - echo "PR Title: ${{ github.event.pull_request.title }}" - echo "Branch: ${{ github.event.pull_request.head.ref }}" - + echo "PR Title: ${GITHUB_EVENT_PULL_REQUEST_TITLE}" + echo "Branch: ${GITHUB_EVENT_PULL_REQUEST_HEAD_REF}" + env: + GITHUB_EVENT_PULL_REQUEST_TITLE: ${{ github.event.pull_request.title }} + GITHUB_EVENT_PULL_REQUEST_HEAD_REF: ${{ github.event.pull_request.head.ref }} + - name: Setup with hardcoded secret env: - API_KEY: "sk-1234567890abcdef" + API_KEY: "${{ secrets.API_KEY }}" # WARNING: Hardcoded secret found and replaced - original key should be revoked run: | echo "Using API key..." - + - name: Run tests run: | npm test diff --git a/.github/workflows/workflow-scanner.yml b/.github/workflows/workflow-scanner.yml index ca779e7f..78ea4974 100644 --- a/.github/workflows/workflow-scanner.yml +++ b/.github/workflows/workflow-scanner.yml @@ -1,32 +1,28 @@ -name: Test Workflow Scanner +name: Workflow Scanner on: - pull_request + push: + branches: + - main jobs: test-scanner: - if: ${{ !contains(github.event.pull_request.title, 'Security Audit & Fixes for GitHub Actions Workflows') }} runs-on: ubuntu-latest permissions: - contents: write - pull-requests: write - issues: write - + contents: read + steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v2 + with: + persist-credentials: false - - name: Login to Docker Hub + # TODO: Pin this action to a commit SHA for security + # Visit https://github.com/docker/login-action/releases to find the real hash for v3 + - name: Docker Login uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Run workflow scanner - id: scanner + # TODO: Pin this action to a commit SHA for security + # Visit https://github.com/Scalabit/workflow-scanner-action/releases to find the real hash for master + - name: Run Workflow Scanner uses: Scalabit/workflow-scanner-action@master - with: - api-token: ${{ secrets.FS_API_TOKEN }} - github-token: ${{ secrets.GH_PAT }} - openai-api-key: ${{ secrets.OPENAI_API_KEY }} - target-branch: main \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go index a72371fe..bf5d7c3b 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -67,6 +67,8 @@ const ( ScannerTimeoutSecs = 3600 // 1 hour timeout ScannerDiskSizeGB = 20 // 20GB boot disk + EmailClientTimeoutSecs = 10 // Timeout for feedback email API calls + DummyNum = 12345 //this is only for testing purposes ) @@ -135,6 +137,12 @@ type WorkflowScanResponse struct { Error string `json:"error,omitempty"` } +type FeedbackRequest struct { + Name string `json:"name"` + Email string `json:"email"` + Message string `json:"message"` +} + var ( // key is "provider:id" (e.g. "github:1234"). premiumUsers = make(map[string]*PremiumUser) @@ -1182,6 +1190,9 @@ func main() { mux.HandleFunc("/api/increment-usage", func(w http.ResponseWriter, r *http.Request) { incrementUsageHandler(w, r) }) + mux.HandleFunc("/api/feedback", func(w http.ResponseWriter, r *http.Request) { + feedbackHandler(w, r) + }) // Scan endpoints removed - scanning is now handled by binary via GitHub Actions mux.HandleFunc("/webhook/stripe", func(w http.ResponseWriter, r *http.Request) { handleStripeWebhook(config, w, r) @@ -1206,6 +1217,106 @@ func main() { log.Fatal(server.ListenAndServe()) } +func sendFeedbackEmail(apiKey string, feedback FeedbackRequest) error { + fromEmail := "info@notifications.scalabit.dev" + toEmail := getEnv("FEEDBACK_TO_EMAIL", "info@notifications.scalabit.dev") + + emailBody := fmt.Sprintf(` +New Feedback from remediator.ai Received + +From: %s (%s) + +Message: +%s +`, feedback.Name, feedback.Email, feedback.Message) + + emailPayload := map[string]interface{}{ + "from": fromEmail, + "to": []string{toEmail}, + "reply_to": feedback.Email, + "subject": fmt.Sprintf("Feedback from %s", feedback.Name), + "text": emailBody, + } + + payloadBytes, err := json.Marshal(emailPayload) + if err != nil { + return fmt.Errorf("failed to marshal email payload: %w", err) + } + + emailReq, err := http.NewRequest(http.MethodPost, "https://api.resend.com/emails", strings.NewReader(string(payloadBytes))) + if err != nil { + return fmt.Errorf("failed to create email request: %w", err) + } + + emailReq.Header.Set("Authorization", "Bearer "+apiKey) + emailReq.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: EmailClientTimeoutSecs * time.Second} + resp, err := client.Do(emailReq) + if err != nil { + return fmt.Errorf("failed to send email: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + body, _ := io.ReadAll(resp.Body) + + return fmt.Errorf("resend API error: %d - %s", resp.StatusCode, string(body)) + } + + return nil +} + +func feedbackHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + + return + } + + var req FeedbackRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON body", http.StatusBadRequest) + + return + } + + if req.Name == "" || req.Email == "" || req.Message == "" { + http.Error(w, "Name, email, and message are required", http.StatusBadRequest) + + return + } + + resendAPIKey := os.Getenv("RESEND_API_KEY") + if resendAPIKey == "" { + log.Printf("RESEND_API_KEY not configured") + http.Error(w, "Email service not configured", http.StatusInternalServerError) + + return + } + + if err := sendFeedbackEmail(resendAPIKey, req); err != nil { + log.Printf("Failed to send feedback email: %v", err) + http.Error(w, "Failed to send feedback", http.StatusInternalServerError) + + return + } + + log.Printf("Feedback received from %s (%s)", req.Name, req.Email) + + response := map[string]interface{}{ + "success": true, + "message": "Feedback sent successfully", + } + + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(response); err != nil { + log.Printf("Failed to encode JSON response: %v", err) + } +} + // updateTokenInDatabase handles the database operations for token revocation. func updateTokenInDatabase(oldToken, newToken, subscriptionID, userLogin string) { database, err := getDatabase() diff --git a/frontend/index.html b/frontend/index.html index 09931b05..fab76aca 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -34,6 +34,29 @@ transform: translateY(-10px); box-shadow: 0 25px 50px -12px rgba(0,0,0,0.15); } + .floating-feedback-btn { + position: fixed; + bottom: 30px; + right: 30px; + z-index: 1000; + transition: all 0.3s ease; + } + .floating-feedback-btn:hover { + transform: scale(1.05); + box-shadow: 0 10px 25px rgba(0, 191, 138, 0.3); + } + .modal-overlay { + display: none; + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 2000; + align-items: center; + justify-content: center; + } + .modal-overlay.active { + display: flex; + }
@@ -181,8 +204,65 @@