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 @@

Secret API Token

+ + + + + \ No newline at end of file diff --git a/frontend/js/app.js b/frontend/js/app.js index 7ce57410..4c0f3a2b 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -4,6 +4,7 @@ class remediatorApp { constructor() { this.auth = new GitHubAuth(); this.gitlabAuth = new GitLabAuth(); + this.feedback = new FeedbackManager(); this.loginBtn = null; } @@ -18,6 +19,9 @@ class remediatorApp { await this.auth.loadConfig(); await this.gitlabAuth.loadConfig(); + // Initialize feedback manager + this.feedback.init(); + // Get DOM elements this.githubBtn = document.getElementById('github-login-btn'); this.gitlabBtn = document.getElementById('gitlab-login-btn'); diff --git a/frontend/js/feedback.js b/frontend/js/feedback.js new file mode 100644 index 00000000..a2edd4f6 --- /dev/null +++ b/frontend/js/feedback.js @@ -0,0 +1,150 @@ +class FeedbackManager { + constructor() { + this.modal = null; + this.form = null; + this.statusElement = null; + } + + init() { + this.modal = document.getElementById('feedback-modal'); + this.form = document.getElementById('feedback-form'); + this.statusElement = document.getElementById('feedback-status'); + + const feedbackBtn = document.getElementById('feedback-btn'); + const closeBtn = document.getElementById('close-feedback-modal'); + const cancelBtn = document.getElementById('cancel-feedback'); + + if (feedbackBtn) { + feedbackBtn.addEventListener('click', () => { + this.openModal(); + }); + } + + if (closeBtn) { + closeBtn.addEventListener('click', () => { + this.closeModal(); + }); + } + + if (cancelBtn) { + cancelBtn.addEventListener('click', () => { + this.closeModal(); + }); + } + + if (this.modal) { + this.modal.addEventListener('click', (e) => { + if (e.target === this.modal) { + this.closeModal(); + } + }); + } + + if (this.form) { + this.form.addEventListener('submit', (e) => { + e.preventDefault(); + this.submitFeedback(); + }); + } + + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && this.modal && this.modal.classList.contains('active')) { + this.closeModal(); + } + }); + } + + openModal() { + const userName = localStorage.getItem('userName'); + const userEmail = localStorage.getItem('userEmail'); + + const nameInput = document.getElementById('feedback-name'); + const emailInput = document.getElementById('feedback-email'); + + if (userName && nameInput) { + nameInput.value = userName; + } + + if (userEmail && emailInput) { + emailInput.value = userEmail; + emailInput.readOnly = true; + emailInput.classList.add('bg-gray-100', 'cursor-not-allowed'); + } + + this.modal.classList.add('active'); + document.body.style.overflow = 'hidden'; + } + + closeModal() { + this.modal.classList.remove('active'); + document.body.style.overflow = ''; + + const emailInput = document.getElementById('feedback-email'); + if (emailInput) { + emailInput.readOnly = false; + emailInput.classList.remove('bg-gray-100', 'cursor-not-allowed'); + } + + this.form.reset(); + this.hideStatus(); + } + + showStatus(message, isError = false) { + this.statusElement.textContent = message; + this.statusElement.className = `text-sm ${isError ? 'text-red-600' : 'text-green-600'}`; + this.statusElement.classList.remove('hidden'); + } + + hideStatus() { + this.statusElement.classList.add('hidden'); + } + + async submitFeedback() { + const name = document.getElementById('feedback-name').value.trim(); + const email = document.getElementById('feedback-email').value.trim(); + const message = document.getElementById('feedback-message').value.trim(); + + if (!name || !email || !message) { + this.showStatus('Please fill in all fields', true); + return; + } + + try { + const submitButton = this.form.querySelector('button[type="submit"]'); + submitButton.disabled = true; + submitButton.textContent = 'Sending...'; + + const response = await fetch('/api/feedback', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name, + email, + message, + }), + }); + + if (!response.ok) { + throw new Error('Failed to send feedback'); + } + + this.showStatus('Thank you! Your feedback has been sent.'); + + setTimeout(() => { + this.closeModal(); + }, 2000); + + } catch (error) { + console.error('Error sending feedback:', error); + this.showStatus('Failed to send feedback. Please try again.', true); + } finally { + const submitButton = this.form.querySelector('button[type="submit"]'); + if (submitButton) { + submitButton.disabled = false; + submitButton.textContent = 'Send Feedback'; + } + } + } +} diff --git a/go.mod b/go.mod index be8a3529..d9fb71b0 100644 --- a/go.mod +++ b/go.mod @@ -1,57 +1,51 @@ -module workflow-scanner +module llm-processor -go 1.25.3 +go 1.25.6 require ( - github.com/99designs/gqlgen v0.17.81 + github.com/99designs/gqlgen v0.17.86 github.com/Khan/genqlient v0.8.1 github.com/lib/pq v1.10.9 + github.com/sashabaranov/go-openai v1.41.2 github.com/stretchr/testify v1.11.1 github.com/stripe/stripe-go/v76 v76.25.0 - github.com/vektah/gqlparser/v2 v2.5.30 - go.opentelemetry.io/otel v1.38.0 - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 - go.opentelemetry.io/otel/log v0.14.0 - go.opentelemetry.io/otel/metric v1.38.0 - go.opentelemetry.io/otel/sdk v1.38.0 - go.opentelemetry.io/otel/sdk/log v0.14.0 - go.opentelemetry.io/otel/sdk/metric v1.38.0 - go.opentelemetry.io/otel/trace v1.38.0 - go.opentelemetry.io/proto/otlp v1.8.0 + github.com/vektah/gqlparser/v2 v2.5.31 + go.opentelemetry.io/otel v1.39.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 + go.opentelemetry.io/otel/log v0.15.0 + go.opentelemetry.io/otel/metric v1.39.0 + go.opentelemetry.io/otel/sdk v1.39.0 + go.opentelemetry.io/otel/sdk/log v0.15.0 + go.opentelemetry.io/otel/sdk/metric v1.39.0 + go.opentelemetry.io/otel/trace v1.39.0 + go.opentelemetry.io/proto/otlp v1.9.0 go.uber.org/mock v0.6.0 - golang.org/x/sync v0.17.0 - google.golang.org/grpc v1.76.0 + golang.org/x/sync v0.19.0 + google.golang.org/grpc v1.78.0 ) require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sosodev/duration v1.3.1 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect - golang.org/x/net v0.44.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect - google.golang.org/protobuf v1.36.9 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 - -replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 - -replace go.opentelemetry.io/otel/log => go.opentelemetry.io/otel/log v0.14.0 - -replace go.opentelemetry.io/otel/sdk/log => go.opentelemetry.io/otel/sdk/log v0.14.0 diff --git a/go.sum b/go.sum index 1f486621..653efc52 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,15 @@ -github.com/99designs/gqlgen v0.17.81 h1:kCkN/xVyRb5rEQpuwOHRTYq83i0IuTQg9vdIiwEerTs= -github.com/99designs/gqlgen v0.17.81/go.mod h1:vgNcZlLwemsUhYim4dC1pvFP5FX0pr2Y+uYUoHFb1ig= +github.com/99designs/gqlgen v0.17.86 h1:C8N3UTa5heXX6twl+b0AJyGkTwYL6dNmFrgZNLRcU6w= +github.com/99designs/gqlgen v0.17.86/go.mod h1:KTrPl+vHA1IUzNlh4EYkl7+tcErL3MgKnhHrBcV74Fw= github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs= github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -20,8 +24,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -30,8 +34,10 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM= +github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= @@ -42,70 +48,70 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stripe/stripe-go/v76 v76.25.0 h1:kmDoOTvdQSTQssQzWZQQkgbAR2Q8eXdMWbN/ylNalWA= github.com/stripe/stripe-go/v76 v76.25.0/go.mod h1:rw1MxjlAKKcZ+3FOXgTHgwiOa2ya6CPq6ykpJ0Q6Po4= -github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE= -github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= -go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= -go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg= -go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= +github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k= +github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 h1:W+m0g+/6v3pa5PgVf2xoFMi5YtNR06WtS7ve5pcvLtM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0/go.mod h1:JM31r0GGZ/GU94mX8hN4D8v6e40aFlUECSQ48HaLgHM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 h1:EKpiGphOYq3CYnIe2eX9ftUkyU+Y8Dtte8OaWyHJ4+I= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0/go.mod h1:nWFP7C+T8TygkTjJ7mAyEaFaE7wNfms3nV/vexZ6qt0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 h1:nKP4Z2ejtHn3yShBb+2KawiXgpn8In5cT7aO2wXuOTE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0/go.mod h1:NwjeBbNigsO4Aj9WgM0C+cKIrxsZUaRmZUO7A8I7u8o= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY= +go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE= +go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ= go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= -go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE= -go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= -google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= -google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= -google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/llm_fix_prompt.md b/llm_fix_prompt.md index 7b50095d..62f866a6 100644 --- a/llm_fix_prompt.md +++ b/llm_fix_prompt.md @@ -1,3 +1,4 @@ +````markdown You are a security expert for GitHub Actions workflows, working on issues that ZIZMOR could not auto-fix. ## CRITICAL RULES @@ -73,13 +74,12 @@ When ZIZMOR reports `unpinned-uses` findings: - **Example fix for unpinned actions:** ```yaml # TODO: Pin to commit SHA - visit https://github.com/actions/checkout/releases to get the real hash for your desired version - uses: actions/checkout@v4 # Consider pinning to commit SHA for security + uses: actions/checkout@v5 # Consider pinning to commit SHA for security ``` - The repository maintainer must manually look up and apply the correct commit SHAs **CRITICAL: DO NOT GENERATE FAKE COMMIT HASHES** - **NEVER replace version tags with made-up commit SHAs** -- **NEVER use patterns like `b5b1e3f5e1c5c1c8d7c5b7e5e5b5e5b5e5b5b5b5`** - **IF an action is unpinned, ADD A TODO COMMENT instead of changing the reference** - **Example of CORRECT handling:** ```yaml @@ -132,4 +132,5 @@ For each fix you make, provide a clear explanation in the `explanations` output 1. What the security issue was 2. How you fixed it 3. Why this fix improves security -4. Any potential impact on workflow functionality \ No newline at end of file +4. Any potential impact on workflow functionality +```` diff --git a/main.go b/main.go index d33e3b6b..12684280 100644 --- a/main.go +++ b/main.go @@ -1,140 +1,434 @@ package main import ( + "bytes" "context" + "encoding/json" "fmt" + "io" + "io/ioutil" + "log" "net/http" "os" - + "path/filepath" "strings" - "workflow-scanner/internal/dagger" - "workflow-scanner/pkg/agent" - daggerImpl "workflow-scanner/pkg/dagger" - "workflow-scanner/pkg/github" - "workflow-scanner/pkg/gitlab" - "workflow-scanner/pkg/zizmor" + "time" + + "github.com/sashabaranov/go-openai" ) -type WorkflowScanner struct{} +type FileChange struct { + Path string `json:"path"` + Content string `json:"content"` +} -// TokenValidationResponse represents the API token validation response. -type TokenValidationResponse struct { - Valid bool `json:"valid"` +type LLMResponse struct { + Explanation string `json:"explanation"` + FileChanges []FileChange `json:"file_changes"` } -// validateAPIToken checks if the API token is valid by calling the web server. -func validateAPIToken(token string) bool { - // Get the server URL from environment, default to localhost for development - serverURL := os.Getenv("TOKEN_VALIDATION_URL") - if serverURL == "" { - serverURL = "http://localhost:8080" // Default for local development +func main() { + log.Println("DEBUG: Starting LLM processor") + log.Printf("DEBUG: OPENAI_API_KEY length: %d", len(os.Getenv("OPENAI_API_KEY"))) + log.Printf("DEBUG: ANTHROPIC_API_KEY length: %d", len(os.Getenv("ANTHROPIC_API_KEY"))) + log.Printf("DEBUG: GEMINI_API_KEY length: %d", len(os.Getenv("GEMINI_API_KEY"))) + log.Printf("DEBUG: MODEL: %s", os.Getenv("MODEL")) + log.Printf("DEBUG: ZIZMOR_ISSUES length: %d", len(os.Getenv("ZIZMOR_ISSUES"))) + + if err := processWorkflows(); err != nil { + log.Fatalf("ERROR: %v", err) } + log.Println("DEBUG: LLM processor completed successfully") +} - // Create request to validate token - req, err := http.NewRequest(http.MethodGet, serverURL+"/api/validate-token", nil) +func processWorkflows() error { + promptContent, issues, err := loadInputData() if err != nil { - return false + return err } - // Set authorization header - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("Content-Type", "application/json") - - // Make the request - client := &http.Client{} - resp, err := client.Do(req) + workflowFiles, err := findWorkflowFiles() if err != nil { - return false + return fmt.Errorf("failed to find workflow files: %w", err) } - defer resp.Body.Close() + log.Printf("DEBUG: Found %d workflow files: %v", len(workflowFiles), workflowFiles) - // Check if token is valid (200 status means valid) - return resp.StatusCode == http.StatusOK + enhancedPrompt := buildEnhancedPrompt(promptContent, issues, workflowFiles) + + // Determine which provider to use based on available API keys + if os.Getenv("OPENAI_API_KEY") != "" { + log.Println("DEBUG: Using OpenAI provider") + client, ctx, cancel, err := createOpenAIClient() + if err != nil { + return err + } + defer cancel() + + resp, err := callOpenAI(ctx, client, enhancedPrompt) + if err != nil { + return err + } + return processOpenAIResponse(resp) + } else if os.Getenv("GEMINI_API_KEY") != "" { + log.Println("DEBUG: Using Gemini provider") + return callGemini(enhancedPrompt) + } else if os.Getenv("ANTHROPIC_API_KEY") != "" { + log.Println("DEBUG: Using Anthropic provider") + return callAnthropic(enhancedPrompt) + } else { + return fmt.Errorf("no API key found (need OPENAI_API_KEY, ANTHROPIC_API_KEY, or GEMINI_API_KEY)") + } } -// ScanAndFixWorkflows scans and fixes workflows with API token validation. -func (m *WorkflowScanner) ScanAndFixWorkflows(ctx context.Context, apiToken *dagger.Secret, githubToken *dagger.Secret, repository string, source *dagger.Directory, targetBranch string) (string, error) { - // Extract and validate API token - tokenValue, err := apiToken.Plaintext(ctx) +func loadInputData() ([]byte, string, error) { + log.Println("DEBUG: Reading prompt file") + promptContent, err := ioutil.ReadFile("llm_fix_prompt.md") if err != nil { - return "", fmt.Errorf("failed to extract API token: %w", err) + return nil, "", fmt.Errorf("failed to read prompt: %w", err) } + log.Printf("DEBUG: Prompt file size: %d bytes", len(promptContent)) - // Validate API token (temporarily disabled for testing) - _ = tokenValue // API token validation temporarily disabled - // if !validateAPIToken(tokenValue) { - // return "", fmt.Errorf("invalid or expired API token - please check your subscription") - // } + issues := os.Getenv("ZIZMOR_ISSUES") + if issues == "" { + issues = "No issues found" + } + const maxIssuePreviewLength = 200 + issuePreview := issues + if len(issues) > maxIssuePreviewLength { + issuePreview = issues[:maxIssuePreviewLength] + "..." + } + log.Printf("DEBUG: Issues: %s", issuePreview) - // Extract GitHub token string - githubTokenStr, err := githubToken.Plaintext(ctx) - if err != nil { - return "", fmt.Errorf("failed to extract GitHub token: %w", err) + return promptContent, issues, nil +} + +func createOpenAIClient() (*openai.Client, context.Context, context.CancelFunc, error) { + log.Println("DEBUG: Creating OpenAI client") + ctx := context.Background() + apiKey := os.Getenv("OPENAI_API_KEY") + if apiKey == "" { + return nil, ctx, func() {}, fmt.Errorf("OPENAI_API_KEY environment variable not set") } + + const apiTimeoutMinutes = 5 + ctx, cancel := context.WithTimeout(ctx, time.Minute*apiTimeoutMinutes) + + client := openai.NewClient(apiKey) + log.Println("DEBUG: OpenAI client created successfully") + + return client, ctx, cancel, nil +} - daggerClient := daggerImpl.NewClient(dag) - zizmor := zizmor.NewZizmor(daggerClient) - agent := agent.NewAgent(daggerClient) +func buildEnhancedPrompt(promptContent []byte, issues string, workflowFiles []string) string { + return fmt.Sprintf(`%s - provider := "github" - if strings.Contains(repository, "gitlab.com") { - provider = "gitlab" +ZIZMOR ISSUES TO FIX: +%s + +WORKFLOW FILES FOUND: +%s + +Please provide your response in the following JSON format: +{ + "explanation": "Brief explanation of what fixes were applied", + "file_changes": [ + { + "path": "relative/path/to/file.yml", + "content": "complete fixed file content" + } + ] +} + +Only include files that need changes in the file_changes array. Provide the complete corrected content for each file.`, + string(promptContent), issues, strings.Join(workflowFiles, "\n")) +} + +func callOpenAI(ctx context.Context, client *openai.Client, enhancedPrompt string) (*openai.ChatCompletionResponse, error) { + log.Println("DEBUG: Sending request to OpenAI API") + const ( + maxTokens = 4000 + lowTemperature = 0.1 + ) + + // Get model from environment or default to gpt-4o + model := os.Getenv("MODEL") + if model == "" { + model = "gpt-4o" } + + req := openai.ChatCompletionRequest{ + Model: model, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleUser, + Content: enhancedPrompt, + }, + }, + MaxTokens: maxTokens, + Temperature: lowTemperature, + } + + resp, err := client.CreateChatCompletion(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to generate content: %w", err) + } + log.Println("DEBUG: Received response from OpenAI API") - var wrapperClient github.WrapperIssueClient - if provider == "gitlab" { - wrapperClient = gitlab.NewWrapperIssueClientImpl(dag, githubTokenStr) - } else { - wrapperClient = github.NewWrapperIssueClientImpl(dag, githubTokenStr) + if len(resp.Choices) == 0 { + return nil, fmt.Errorf("no response generated from OpenAI") } + log.Printf("DEBUG: Response has %d choices", len(resp.Choices)) - return scanAndFixWorflowsImpl(ctx, repository, source, zizmor, agent, wrapperClient, targetBranch) + return &resp, nil } -func scanAndFixWorflowsImpl(ctx context.Context, repository string, source *dagger.Directory, zizmor zizmor.Zizmor, agent agent.Agent, githubClient github.WrapperIssueClient, targetBranch string) (string, error) { - autoFixedDirectory, zizmorFindings, fixSummary, err := zizmor.RunZizmorAutoFix(ctx, source) - if err != nil { - return "", fmt.Errorf("failed to run ZIZMOR auto-fix: %w", err) +func processOpenAIResponse(resp *openai.ChatCompletionResponse) error { + responseText := resp.Choices[0].Message.Content + + log.Println("DEBUG: Parsing response as JSON") + var llmResponse LLMResponse + if err := parseJSONResponse(responseText, &llmResponse); err != nil { + log.Printf("DEBUG: JSON parsing failed: %v", err) + log.Println("DEBUG: Returning raw response text") + fmt.Print(responseText) + return nil } - remainingIssues, err := zizmor.CheckRemainingIssues(ctx, autoFixedDirectory) - if err != nil { - return "", fmt.Errorf("failed to check remaining issues: %w", err) + log.Printf("DEBUG: Applying %d file changes", len(llmResponse.FileChanges)) + for i, change := range llmResponse.FileChanges { + log.Printf("DEBUG: Applying change %d/%d to %s", i+1, len(llmResponse.FileChanges), change.Path) + if err := applyFileChange(change); err != nil { + log.Printf("Warning: Failed to apply change to %s: %v", change.Path, err) + } } - finalDirectory := autoFixedDirectory + log.Printf("DEBUG: Returning explanation: %d chars", len(llmResponse.Explanation)) + fmt.Print(llmResponse.Explanation) + + return nil +} - llmExplanations := "" - if remainingIssues != "" && remainingIssues != "[]" && remainingIssues != "[]\n" { - finalDirectory, llmExplanations, err = agent.FixRemainingIssues(ctx, autoFixedDirectory, remainingIssues) +func findWorkflowFiles() ([]string, error) { + var files []string + err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { if err != nil { - return "", fmt.Errorf("failed to fix remaining issues with LLM: %w", err) + return err } + if strings.HasSuffix(path, ".yml") || strings.HasSuffix(path, ".yaml") { + files = append(files, path) + } + return nil + }) + + return files, err +} + +func parseJSONResponse(responseText string, llmResponse *LLMResponse) error { + // Find JSON content between ```json and ``` markers + start := strings.Index(responseText, "```json") + if start == -1 { + start = strings.Index(responseText, "{") } else { - llmExplanations = "No remaining security issues found after ZIZMOR auto-fix" + start += 7 // skip ```json + } + + end := strings.LastIndex(responseText, "}") + if start == -1 || end == -1 || start >= end { + + return fmt.Errorf("no valid JSON found in response") } - // Run final validation scan on the fixed code - finalValidation, err := zizmor.CheckRemainingIssues(ctx, finalDirectory) + jsonStr := strings.TrimSpace(responseText[start : end+1]) + + return json.Unmarshal([]byte(jsonStr), llmResponse) +} + +func applyFileChange(change FileChange) error { + // Ensure the directory exists + const ( + dirPermissions = 0755 + filePermissions = 0644 + ) + dir := filepath.Dir(change.Path) + if err := os.MkdirAll(dir, dirPermissions); err != nil { + return fmt.Errorf("failed to create directory %s: %w", dir, err) + } + + // Write the file + if err := ioutil.WriteFile(change.Path, []byte(change.Content), filePermissions); err != nil { + return fmt.Errorf("failed to write file %s: %w", change.Path, err) + } + + log.Printf("Applied fix to %s", change.Path) + + return nil +} + +func callGemini(enhancedPrompt string) error { + apiKey := os.Getenv("GEMINI_API_KEY") + model := os.Getenv("MODEL") + if model == "" { + model = "gemini-2.5-flash" + } + + log.Printf("DEBUG: Calling Gemini API with model: %s", model) + + // Gemini API request structure + requestBody := map[string]interface{}{ + "contents": []map[string]interface{}{ + { + "parts": []map[string]interface{}{ + { + "text": enhancedPrompt, + }, + }, + }, + }, + "generationConfig": map[string]interface{}{ + "temperature": 0.1, + "maxOutputTokens": 32000, + }, + } + + jsonData, err := json.Marshal(requestBody) + if err != nil { + return fmt.Errorf("failed to marshal request: %w", err) + } + + url := fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent?key=%s", model, apiKey) + resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData)) if err != nil { - return "", fmt.Errorf("failed to run final validation scan: %w", err) + return fmt.Errorf("failed to call Gemini API: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + body, _ := io.ReadAll(resp.Body) + log.Printf("DEBUG: Gemini API error response: %s", string(body)) + return fmt.Errorf("Gemini API returned status %d: %s", resp.StatusCode, string(body)) } + + var response map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return fmt.Errorf("failed to decode Gemini response: %w", err) + } + + // Debug: Log the full response structure + responseJSON, _ := json.MarshalIndent(response, "", " ") + log.Printf("DEBUG: Full Gemini response: %s", string(responseJSON)) + + // Extract text from Gemini response + candidates, ok := response["candidates"].([]interface{}) + if !ok || len(candidates) == 0 { + return fmt.Errorf("no candidates in Gemini response") + } + + candidate := candidates[0].(map[string]interface{}) + content := candidate["content"].(map[string]interface{}) + parts := content["parts"].([]interface{}) + if len(parts) == 0 { + return fmt.Errorf("no parts in Gemini response") + } + + part := parts[0].(map[string]interface{}) + text := part["text"].(string) + + log.Printf("DEBUG: Gemini response received, length: %d", len(text)) + + // Parse and process the response using the same logic as OpenAI + var llmResponse LLMResponse + if err := parseJSONResponse(text, &llmResponse); err != nil { + log.Printf("DEBUG: Gemini JSON parsing failed: %v", err) + log.Printf("DEBUG: Raw Gemini response: %s", text) + return fmt.Errorf("failed to parse JSON from Gemini response: %w", err) + } + + return processGenericResponse(&llmResponse) +} - // Scan external repositories used in workflows - fullRepoFindings, err := zizmor.ScanExternalDependencies(ctx, finalDirectory) - summaryExternalFindings := zizmor.SummarizeExternalFindings(fullRepoFindings) +func callAnthropic(enhancedPrompt string) error { + apiKey := os.Getenv("ANTHROPIC_API_KEY") + model := os.Getenv("MODEL") + if model == "" { + model = "claude-3-5-sonnet-20241022" + } + + log.Printf("DEBUG: Calling Anthropic API with model: %s", model) + + // Anthropic API request structure + requestBody := map[string]interface{}{ + "model": model, + "max_tokens": 4000, + "temperature": 0.1, + "messages": []map[string]interface{}{ + { + "role": "user", + "content": enhancedPrompt, + }, + }, + } + + jsonData, err := json.Marshal(requestBody) + if err != nil { + return fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", bytes.NewBuffer(jsonData)) if err != nil { - summaryExternalFindings = fmt.Sprintf("Failed to scan external dependencies: %s", err.Error()) + return fmt.Errorf("failed to create request: %w", err) } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-api-key", apiKey) + req.Header.Set("anthropic-version", "2023-06-01") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to call Anthropic API: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("Anthropic API returned status %d", resp.StatusCode) + } + + var response map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return fmt.Errorf("failed to decode Anthropic response: %w", err) + } + + // Extract text from Anthropic response + content, ok := response["content"].([]interface{}) + if !ok || len(content) == 0 { + return fmt.Errorf("no content in Anthropic response") + } + + contentItem := content[0].(map[string]interface{}) + text := contentItem["text"].(string) + + log.Printf("DEBUG: Anthropic response received, length: %d", len(text)) + + // Parse and process the response using the same logic as OpenAI + var llmResponse LLMResponse + if err := parseJSONResponse(text, &llmResponse); err != nil { + return fmt.Errorf("failed to parse JSON from Anthropic response: %w", err) + } + + return processGenericResponse(&llmResponse) +} - // Truncate external findings if too long to fit GitHub's 65,536 char limit - maxExternalLength := 20000 // Leave room for other content - if len(summaryExternalFindings) > maxExternalLength { - summaryExternalFindings = summaryExternalFindings[:maxExternalLength] + - "\n\n... (truncated due to length - see full scan in workflow logs)" +func processGenericResponse(llmResponse *LLMResponse) error { + log.Printf("DEBUG: Applying %d file changes", len(llmResponse.FileChanges)) + for i, change := range llmResponse.FileChanges { + log.Printf("DEBUG: Applying change %d/%d to %s", i+1, len(llmResponse.FileChanges), change.Path) + if err := applyFileChange(change); err != nil { + log.Printf("Warning: Failed to apply change to %s: %v", change.Path, err) + } } - prTitle, prBody := github.GetPrTitleBody(finalValidation, zizmorFindings, fixSummary, llmExplanations, summaryExternalFindings) + log.Printf("DEBUG: Returning explanation: %d chars", len(llmResponse.Explanation)) + fmt.Print(llmResponse.Explanation) - return githubClient.CreatePullRequest(ctx, repository, prTitle, prBody, finalDirectory, targetBranch) + return nil } diff --git a/terraform/environments/production/main.tf b/terraform/environments/production/main.tf index e35d3ca6..38a945c8 100644 --- a/terraform/environments/production/main.tf +++ b/terraform/environments/production/main.tf @@ -117,6 +117,8 @@ locals { "stripe-publishable-key" = var.stripe_publishable_key "stripe-webhook-secret" = var.stripe_webhook_secret "openai-api-key" = var.openai_api_key + "resend-api-key" = var.resend_api_key + "feedback-to-email" = var.feedback_to_email } } @@ -294,6 +296,27 @@ resource "google_cloud_run_v2_service" "workflow_scanner" { } } } + + # Feedback/Email configuration + env { + name = "RESEND_API_KEY" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.secrets["resend-api-key"].secret_id + version = "latest" + } + } + } + + env { + name = "FEEDBACK_TO_EMAIL" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.secrets["feedback-to-email"].secret_id + version = "latest" + } + } + } } } diff --git a/terraform/environments/production/variables.tf b/terraform/environments/production/variables.tf index 020964fa..66bc4d92 100644 --- a/terraform/environments/production/variables.tf +++ b/terraform/environments/production/variables.tf @@ -65,6 +65,18 @@ variable "openai_api_key" { sensitive = true } +variable "resend_api_key" { + description = "Resend API Key for sending emails" + type = string + sensitive = true +} + +variable "feedback_to_email" { + description = "Email address to receive feedback" + type = string + default = "info@notifications.scalabit.dev" +} + variable "service_name" { description = "Name of the Cloud Run service" diff --git a/terraform/environments/sandbox/main.tf b/terraform/environments/sandbox/main.tf index 75a915ae..ce236619 100644 --- a/terraform/environments/sandbox/main.tf +++ b/terraform/environments/sandbox/main.tf @@ -115,6 +115,8 @@ locals { "sandbox-stripe-publishable-key" = var.stripe_publishable_key "sandbox-stripe-webhook-secret" = var.stripe_webhook_secret "sandbox-openai-api-key" = var.openai_api_key + "sandbox-resend-api-key" = var.resend_api_key + "sandbox-feedback-to-email" = var.feedback_to_email "sandbox-allowed-users" = var.sandbox_allowed_users } } @@ -284,6 +286,27 @@ resource "google_cloud_run_v2_service" "workflow_scanner" { } } + # Feedback/Email configuration + env { + name = "RESEND_API_KEY" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.secrets["sandbox-resend-api-key"].secret_id + version = "latest" + } + } + } + + env { + name = "FEEDBACK_TO_EMAIL" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.secrets["sandbox-feedback-to-email"].secret_id + version = "latest" + } + } + } + # Sandbox access control env { name = "ENVIRONMENT" diff --git a/terraform/environments/sandbox/variables.tf b/terraform/environments/sandbox/variables.tf index 4f7fc283..f598819b 100644 --- a/terraform/environments/sandbox/variables.tf +++ b/terraform/environments/sandbox/variables.tf @@ -54,6 +54,18 @@ variable "openai_api_key" { sensitive = true } +variable "resend_api_key" { + description = "Resend API Key for sending emails" + type = string + sensitive = true +} + +variable "feedback_to_email" { + description = "Email address to receive feedback" + type = string + default = "info@notifications.scalabit.dev" +} + variable "sandbox_allowed_users" { description = "Comma-separated list of email addresses allowed to access sandbox environment"