Skip to content

Commit 2195d1a

Browse files
committed
feat: secureprompt: Pre-flight security layer for AI prompts
0 parents  commit 2195d1a

File tree

24 files changed

+1649
-0
lines changed

24 files changed

+1649
-0
lines changed

Makefile

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
.PHONY: build run test clean fmt vet lint
2+
3+
BINARY = secureprompt
4+
CMD = ./cmd/secureprompt
5+
6+
## build: Compile the binary
7+
build:
8+
go build -o $(BINARY) $(CMD)
9+
10+
## run: Run in development mode
11+
run:
12+
go run $(CMD)
13+
14+
## test: Run all tests
15+
test:
16+
go test ./... -v -count=1
17+
18+
## fmt: Format all Go files
19+
fmt:
20+
gofmt -s -w .
21+
22+
## vet: Run go vet
23+
vet:
24+
go vet ./...
25+
26+
## lint: Format + vet
27+
lint: fmt vet
28+
29+
## clean: Remove build artifacts
30+
clean:
31+
rm -f $(BINARY)
32+
33+
## health: Check if the API is running
34+
health:
35+
@curl -s http://localhost:8080/health | python3 -m json.tool
36+
37+
## scan: Quick interactive scan (usage: make scan PROMPT="your text")
38+
scan:
39+
@curl -s http://localhost:8080/v1/prescan \
40+
-H 'Content-Type: application/json' \
41+
-d '{"content":"$(PROMPT)"}' | python3 -m json.tool
42+
43+
## stats: View scan statistics
44+
stats:
45+
@curl -s http://localhost:8080/v1/stats | python3 -m json.tool
46+
47+
## audit: View the audit log
48+
audit:
49+
@curl -s http://localhost:8080/v1/audit | python3 -m json.tool
50+
51+
## help: Show this help
52+
help:
53+
@grep -E '^## ' Makefile | sed 's/## //' | column -t -s ':'

README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
![SecurePrompt](secureprompt-logo-banner.png)
2+
# SecurePrompt
3+
4+
**Pre-flight security layer for AI prompts.**
5+
6+
Scans every prompt for secrets, PII, prompt injection, risky operations, data exfiltration, and malware intent — before it reaches your LLM.
7+
8+
## Quick Start
9+
10+
```bash
11+
# Build and run
12+
make build
13+
./secureprompt
14+
15+
# Or run directly
16+
make run
17+
```
18+
19+
## Test
20+
21+
```bash
22+
# Safe prompt
23+
make scan PROMPT="Write hello world in Go"
24+
25+
# Secret detected → BLOCK
26+
make scan PROMPT="My key is sk-abc123xyz456"
27+
28+
# Injection → REVIEW
29+
make scan PROMPT="Ignore all previous instructions"
30+
31+
# Run full test suite
32+
bash scripts/test_examples.sh
33+
```
34+
35+
### Architecture
36+
![alt text](secureprompt_architecture.png)
37+
38+
## API
39+
40+
| Method | Path | Description |
41+
|--------|------|-------------|
42+
| GET | `/health` | Health check |
43+
| POST | `/v1/prescan` | Scan a prompt |
44+
| GET | `/v1/audit` | View audit log |
45+
| GET | `/v1/stats` | View statistics |
46+
47+
## Zero Dependencies
48+
49+
The entire project uses **only Go's standard library**. No external packages.
50+
51+
## License
52+
53+
MIT

cmd/secureprompt/main.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// SecurePrompt — Pre-flight security layer for AI prompts.
2+
//
3+
// Usage:
4+
//
5+
// go run ./cmd/secureprompt
6+
// # or
7+
// go build -o secureprompt ./cmd/secureprompt && ./secureprompt
8+
package main
9+
10+
import (
11+
"fmt"
12+
"log"
13+
"net/http"
14+
"os"
15+
16+
"github.com/ravisastryk/secureprompt/internal/api"
17+
)
18+
19+
func main() {
20+
port := os.Getenv("PORT")
21+
if port == "" {
22+
port = "8080"
23+
}
24+
25+
hmacSecret := os.Getenv("HMAC_SECRET")
26+
if hmacSecret == "" {
27+
hmacSecret = "secureprompt-dev-secret"
28+
}
29+
30+
srv := api.NewServer(hmacSecret)
31+
32+
mux := http.NewServeMux()
33+
srv.RegisterRoutes(mux)
34+
35+
fmt.Println("╔══════════════════════════════════════════════════════════════╗")
36+
fmt.Println("║ SecurePrompt API v1.0 ║")
37+
fmt.Println("╠══════════════════════════════════════════════════════════════╣")
38+
fmt.Printf("║ Port: %-49s ║\n", port)
39+
fmt.Println("║ Policy: strict (default) ║")
40+
fmt.Println("╠══════════════════════════════════════════════════════════════╣")
41+
fmt.Println("║ Endpoints: ║")
42+
fmt.Println("║ GET /health -> Health check ║")
43+
fmt.Println("║ POST /v1/prescan -> Scan a prompt ║")
44+
fmt.Println("║ GET /v1/audit -> View audit log ║")
45+
fmt.Println("║ GET /v1/stats -> View statistics ║")
46+
fmt.Println("╚══════════════════════════════════════════════════════════════╝")
47+
48+
log.Fatal(http.ListenAndServe(":"+port, mux))
49+
}

configs/openapi.json

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"openapi": "3.1.0",
3+
"info": {
4+
"title": "SecurePrompt API",
5+
"description": "Pre-flight security scanning for AI prompts",
6+
"version": "1.0.0"
7+
},
8+
"servers": [
9+
{
10+
"url": "YOUR_NGROK_URL_HERE",
11+
"description": "SecurePrompt Server"
12+
}
13+
],
14+
"paths": {
15+
"/v1/prescan": {
16+
"post": {
17+
"operationId": "prescan",
18+
"summary": "Scan a prompt for security issues",
19+
"requestBody": {
20+
"required": true,
21+
"content": {
22+
"application/json": {
23+
"schema": {
24+
"type": "object",
25+
"required": ["content"],
26+
"properties": {
27+
"event_id": { "type": "string" },
28+
"content": { "type": "string", "description": "The prompt to scan" },
29+
"policy_profile": { "type": "string", "enum": ["strict", "moderate", "permissive"], "default": "strict" }
30+
}
31+
}
32+
}
33+
}
34+
},
35+
"responses": {
36+
"200": {
37+
"description": "Scan result",
38+
"content": {
39+
"application/json": {
40+
"schema": {
41+
"type": "object",
42+
"properties": {
43+
"event_id": { "type": "string" },
44+
"risk_level": { "type": "string", "enum": ["SAFE", "REVIEW", "BLOCK"] },
45+
"risk_score": { "type": "integer" },
46+
"findings": { "type": "array" },
47+
"safe_rewrite": { "type": "string" },
48+
"decision_signature": { "type": "string" }
49+
}
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}
56+
}
57+
}
58+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/ravisastryk/secureprompt
2+
3+
go 1.22

internal/api/handler.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Package api wires together the detection engine, policy engine,
2+
// rewriter, and audit logger behind HTTP endpoints.
3+
package api
4+
5+
import (
6+
"encoding/json"
7+
"log"
8+
"net/http"
9+
"sync"
10+
"time"
11+
12+
"github.com/ravisastryk/secureprompt/internal/audit"
13+
"github.com/ravisastryk/secureprompt/internal/detector"
14+
"github.com/ravisastryk/secureprompt/internal/middleware"
15+
"github.com/ravisastryk/secureprompt/internal/models"
16+
"github.com/ravisastryk/secureprompt/internal/policy"
17+
"github.com/ravisastryk/secureprompt/internal/rewriter"
18+
"github.com/ravisastryk/secureprompt/internal/util"
19+
)
20+
21+
// Server holds all dependencies for the API.
22+
type Server struct {
23+
detector *detector.Engine
24+
policy *policy.Engine
25+
rewriter *rewriter.Engine
26+
audit *audit.Logger
27+
28+
mu sync.RWMutex
29+
stats Stats
30+
}
31+
32+
// Stats tracks scan metrics.
33+
type Stats struct {
34+
TotalScans int `json:"total_scans"`
35+
ByDecision map[string]int `json:"by_decision"`
36+
}
37+
38+
// NewServer creates a fully-wired API server.
39+
func NewServer(hmacSecret string) *Server {
40+
return &Server{
41+
detector: detector.NewEngine(),
42+
policy: policy.NewEngine(),
43+
rewriter: rewriter.NewEngine(),
44+
audit: audit.NewLogger(hmacSecret),
45+
stats: Stats{ByDecision: map[string]int{"SAFE": 0, "REVIEW": 0, "BLOCK": 0}},
46+
}
47+
}
48+
49+
// RegisterRoutes mounts all endpoints on the default mux.
50+
func (s *Server) RegisterRoutes(mux *http.ServeMux) {
51+
mux.HandleFunc("/health", middleware.CORS(s.handleHealth))
52+
mux.HandleFunc("/v1/prescan", middleware.CORS(s.handlePrescan))
53+
mux.HandleFunc("/v1/audit", middleware.CORS(s.handleAudit))
54+
mux.HandleFunc("/v1/stats", middleware.CORS(s.handleStats))
55+
56+
// Serve dashboard from web/static/ (falls back to embedded index if dir missing)
57+
fs := http.FileServer(http.Dir("web/static"))
58+
mux.Handle("/", fs)
59+
}
60+
61+
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
62+
util.WriteJSON(w, http.StatusOK, map[string]string{
63+
"status": "ok",
64+
"service": "secureprompt",
65+
"version": "1.0.0",
66+
})
67+
}
68+
69+
func (s *Server) handlePrescan(w http.ResponseWriter, r *http.Request) {
70+
if r.Method != http.MethodPost {
71+
util.WriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "POST required"})
72+
return
73+
}
74+
75+
var req models.PrescanRequest
76+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
77+
util.WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
78+
return
79+
}
80+
if req.Content == "" {
81+
util.WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "content is required"})
82+
return
83+
}
84+
85+
// Defaults
86+
if req.EventID == "" {
87+
req.EventID = "evt_" + util.ShortUUID()
88+
}
89+
if req.PolicyProfile == "" {
90+
req.PolicyProfile = "strict"
91+
}
92+
93+
start := time.Now()
94+
95+
// 1. Detect
96+
findings := s.detector.Scan(req.Content)
97+
98+
// 2. Policy decision
99+
decision := s.policy.Evaluate(req.PolicyProfile, findings)
100+
101+
// 3. Rewrite (only for REVIEW/BLOCK with findings)
102+
safeRewrite := ""
103+
if decision.RiskLevel != models.RiskSafe && len(findings) > 0 {
104+
safeRewrite = s.rewriter.Rewrite(req.Content, findings)
105+
}
106+
107+
// 4. Audit log
108+
sig := s.audit.Log(req.EventID, decision.RiskLevel, decision.RiskScore, len(findings), req.PolicyProfile)
109+
110+
// 5. Stats
111+
s.mu.Lock()
112+
s.stats.TotalScans++
113+
s.stats.ByDecision[string(decision.RiskLevel)]++
114+
s.mu.Unlock()
115+
116+
elapsed := time.Since(start)
117+
118+
// If no findings, return a single OK finding for clarity
119+
if len(findings) == 0 {
120+
findings = []models.Finding{{
121+
Category: models.CategoryOK,
122+
Type: "NONE",
123+
Detail: "No security issues detected",
124+
Confidence: 1.0,
125+
Severity: "none",
126+
}}
127+
}
128+
129+
resp := models.PrescanResponse{
130+
EventID: req.EventID,
131+
TenantID: req.TenantID,
132+
SessionID: req.SessionID,
133+
PolicyProfile: req.PolicyProfile,
134+
RiskLevel: decision.RiskLevel,
135+
RiskScore: decision.RiskScore,
136+
Findings: findings,
137+
SafeRewrite: safeRewrite,
138+
Timestamp: time.Now().UTC().Format(time.RFC3339),
139+
ProcessingTimeMs: elapsed.Milliseconds(),
140+
DecisionSignature: sig,
141+
}
142+
143+
log.Printf("[%s] %s | score=%d | findings=%d | %dms",
144+
resp.RiskLevel, req.EventID, resp.RiskScore, len(findings), elapsed.Milliseconds())
145+
146+
util.WriteJSON(w, http.StatusOK, resp)
147+
}
148+
149+
func (s *Server) handleAudit(w http.ResponseWriter, r *http.Request) {
150+
util.WriteJSON(w, http.StatusOK, map[string]interface{}{"entries": s.audit.Entries()})
151+
}
152+
153+
func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
154+
s.mu.RLock()
155+
defer s.mu.RUnlock()
156+
util.WriteJSON(w, http.StatusOK, s.stats)
157+
}

0 commit comments

Comments
 (0)