Skip to content

Commit 4b4d217

Browse files
authored
Log reflect responses for detection debugging (#122)
1 parent dabcb3e commit 4b4d217

4 files changed

Lines changed: 54 additions & 0 deletions

File tree

.github/workflows/detection-only.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ jobs:
222222
# The api-proxy hostname returns 403 from this detection-only AWF path; target the internal api-proxy IP instead.
223223
# THREAT_DETECTION_REFLECT_URL: http://api-proxy:10000/reflect
224224
THREAT_DETECTION_REFLECT_URL: http://172.30.0.30:10000/reflect
225+
THREAT_DETECTION_LOG_REFLECT_RESPONSE: "true"
225226
WORKFLOW_DESCRIPTION: "Smoke test workflow that validates Copilot engine execution in this repository"
226227
WORKFLOW_NAME: ${{ github.workflow }}
227228
run: |

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ responses fail safe into the full detector. The full detector preserves the
7575
existing CLI engine behavior and prefers `/reflect` structured output when a
7676
schema-capable model is available.
7777

78+
Set `THREAT_DETECTION_LOG_REFLECT_RESPONSE=true` to print raw `/reflect`
79+
responses to stderr for controlled debugging. Responses may include reflected
80+
artifact content, so leave this disabled outside targeted diagnostic runs.
81+
7882
**Exit codes:**
7983
- `0` — Safe (no threats detected)
8084
- `1` — Threat detected

pkg/engine/reflect.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88
"io"
99
"net/http"
1010
"net/url"
11+
"os"
1112
"sort"
13+
"strconv"
1214
"strings"
1315
"time"
1416

@@ -106,6 +108,7 @@ func (c *ReflectClient) ListModels(ctx context.Context) ([]ReflectModel, error)
106108
if err != nil {
107109
return nil, fmt.Errorf("reading reflect model list: %w", err)
108110
}
111+
printReflectResponse(os.Stderr, http.MethodGet, resp.Status, body)
109112
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
110113
return nil, fmt.Errorf("listing reflect models returned %s: %s", resp.Status, responseBodyPreview(body))
111114
}
@@ -135,12 +138,33 @@ func (c *ReflectClient) postReflect(ctx context.Context, payload map[string]any)
135138
if err != nil {
136139
return nil, fmt.Errorf("reading reflect response: %w", err)
137140
}
141+
printReflectResponse(os.Stderr, http.MethodPost, resp.Status, body)
138142
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
139143
return nil, fmt.Errorf("reflect returned %s: %s", resp.Status, responseBodyPreview(body))
140144
}
141145
return body, nil
142146
}
143147

148+
func printReflectResponse(w io.Writer, method, status string, body []byte) {
149+
if !logReflectResponseEnabled() {
150+
return
151+
}
152+
fmt.Fprintf(w, "::group::/reflect %s response (%s)\n%s", method, status, string(body))
153+
if !bytes.HasSuffix(body, []byte("\n")) {
154+
fmt.Fprintln(w)
155+
}
156+
fmt.Fprintln(w, "::endgroup::")
157+
}
158+
159+
func logReflectResponseEnabled() bool {
160+
value := strings.TrimSpace(os.Getenv("THREAT_DETECTION_LOG_REFLECT_RESPONSE"))
161+
if value == "" {
162+
return false
163+
}
164+
enabled, err := strconv.ParseBool(value)
165+
return err == nil && enabled
166+
}
167+
144168
func (c *ReflectClient) endpoint() string {
145169
base := strings.TrimSpace(c.BaseURL)
146170
if base == "" {

pkg/engine/reflect_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package engine
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/json"
67
"net/http"
@@ -134,6 +135,30 @@ func TestReflectClient_TruncatesNon2xxReflectError(t *testing.T) {
134135
}
135136
}
136137

138+
func TestPrintReflectResponseHonorsEnv(t *testing.T) {
139+
var buf bytes.Buffer
140+
141+
printReflectResponse(&buf, http.MethodGet, "200 OK", []byte(`{"models":[]}`))
142+
if buf.Len() != 0 {
143+
t.Fatalf("expected no debug output when env var is unset, got %q", buf.String())
144+
}
145+
146+
buf.Reset()
147+
t.Setenv("THREAT_DETECTION_LOG_REFLECT_RESPONSE", "true")
148+
printReflectResponse(&buf, http.MethodGet, "200 OK", []byte(`{"models":[]}`))
149+
150+
got := buf.String()
151+
for _, want := range []string{
152+
"::group::/reflect GET response (200 OK)",
153+
`{"models":[]}`,
154+
"::endgroup::",
155+
} {
156+
if !strings.Contains(got, want) {
157+
t.Fatalf("debug output missing %q: %q", want, got)
158+
}
159+
}
160+
}
161+
137162
func TestReflectClient_DoesNotParseEchoedInput(t *testing.T) {
138163
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
139164
if r.Method == http.MethodGet {

0 commit comments

Comments
 (0)