mirror of
https://github.com/lunchcat/sif.git
synced 2026-07-05 20:27:03 -07:00
122fe5f94b
add recon modules for unauthenticated gpu serving stacks that leak model info without a key: triton, sglang, and torchserve.
162 lines
6.1 KiB
Go
162 lines
6.1 KiB
Go
package modules_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/vmfunc/sif/internal/modules"
|
|
)
|
|
|
|
func runGPUServingModule(t *testing.T, file string, status int, body string) *modules.Result {
|
|
t.Helper()
|
|
def, err := modules.ParseYAMLModule(file)
|
|
if err != nil {
|
|
t.Fatalf("parse %s: %v", file, err)
|
|
}
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(status)
|
|
_, _ = w.Write([]byte(body))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
|
|
Timeout: 5 * time.Second,
|
|
Threads: 2,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("execute %s: %v", file, err)
|
|
}
|
|
return res
|
|
}
|
|
|
|
func gpuServingExtract(res *modules.Result, key string) string {
|
|
for _, f := range res.Findings {
|
|
if v := f.Extracted[key]; v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func TestLLMGPUServingExposureModules(t *testing.T) {
|
|
const triton = "../../modules/recon/triton-api-exposure.yaml"
|
|
const sglang = "../../modules/recon/sglang-api-exposure.yaml"
|
|
const torchserve = "../../modules/recon/torchserve-api-exposure.yaml"
|
|
|
|
tritonMeta := `{"name":"triton","version":"2.45.0","extensions":["classification","sequence",` +
|
|
`"model_repository","schedule_policy","model_configuration","statistics","trace","logging"]}`
|
|
|
|
sglangInfo := `{"model_path":"meta-llama/Llama-3-8B","tokenizer_path":"meta-llama/Llama-3-8B",` +
|
|
`"is_generation":true,"preferred_sampling_params":null,"weight_version":"default",` +
|
|
`"has_image_understanding":false,"has_audio_understanding":false,"model_type":"llama",` +
|
|
`"architectures":["LlamaForCausalLM"]}`
|
|
|
|
torchserveModels := `{"nextPageToken":"4","models":[{"modelName":"resnet-18","modelUrl":"resnet-18.mar"},` +
|
|
`{"modelName":"noop","modelUrl":"noop-v1.0"}]}`
|
|
|
|
t.Run("a triton metadata api is flagged with its version", func(t *testing.T) {
|
|
res := runGPUServingModule(t, triton, 200, tritonMeta)
|
|
if len(res.Findings) == 0 {
|
|
t.Fatal("expected a triton finding")
|
|
}
|
|
if v := gpuServingExtract(res, "triton_version"); v != "2.45.0" {
|
|
t.Errorf("triton_version=%q, want 2.45.0", v)
|
|
}
|
|
})
|
|
|
|
t.Run("a sglang model_info is flagged with its model path", func(t *testing.T) {
|
|
res := runGPUServingModule(t, sglang, 200, sglangInfo)
|
|
if len(res.Findings) == 0 {
|
|
t.Fatal("expected a sglang finding")
|
|
}
|
|
if v := gpuServingExtract(res, "sglang_model"); v != "meta-llama/Llama-3-8B" {
|
|
t.Errorf("sglang_model=%q, want meta-llama/Llama-3-8B", v)
|
|
}
|
|
})
|
|
|
|
t.Run("a torchserve models api is flagged with its model name", func(t *testing.T) {
|
|
res := runGPUServingModule(t, torchserve, 200, torchserveModels)
|
|
if len(res.Findings) == 0 {
|
|
t.Fatal("expected a torchserve finding")
|
|
}
|
|
if v := gpuServingExtract(res, "torchserve_model"); v != "resnet-18" {
|
|
t.Errorf("torchserve_model=%q, want resnet-18", v)
|
|
}
|
|
})
|
|
|
|
t.Run("a hugging face config with model_type is not flagged as sglang", func(t *testing.T) {
|
|
body := `{"model_type":"llama","architectures":["LlamaForCausalLM"],"hidden_size":4096}`
|
|
if res := runGPUServingModule(t, sglang, 200, body); len(res.Findings) > 0 {
|
|
t.Errorf("a model config should not match sglang, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("a generation flag alone is not flagged as sglang", func(t *testing.T) {
|
|
body := `{"is_generation":true,"model":"x"}`
|
|
if res := runGPUServingModule(t, sglang, 200, body); len(res.Findings) > 0 {
|
|
t.Errorf("is_generation alone should not match sglang, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("an image understanding flag alone is not flagged as sglang", func(t *testing.T) {
|
|
body := `{"has_image_understanding":false,"model_path":"x"}`
|
|
if res := runGPUServingModule(t, sglang, 200, body); len(res.Findings) > 0 {
|
|
t.Errorf("has_image_understanding alone should not match sglang, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("a model url without a page token is not flagged as torchserve", func(t *testing.T) {
|
|
body := `{"models":[{"modelName":"x","modelUrl":"x.mar"}]}`
|
|
if res := runGPUServingModule(t, torchserve, 200, body); len(res.Findings) > 0 {
|
|
t.Errorf("modelUrl without nextPageToken should not match torchserve, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("an ollama style models list is not flagged as torchserve", func(t *testing.T) {
|
|
body := `{"models":[{"name":"llama3:latest","model":"llama3:latest"}]}`
|
|
if res := runGPUServingModule(t, torchserve, 200, body); len(res.Findings) > 0 {
|
|
t.Errorf("an ollama model list should not match torchserve, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("a paginated list without a model url is not flagged as torchserve", func(t *testing.T) {
|
|
body := `{"nextPageToken":"4","items":[{"id":"a"}]}`
|
|
if res := runGPUServingModule(t, torchserve, 200, body); len(res.Findings) > 0 {
|
|
t.Errorf("a generic paginated list should not match torchserve, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("a kserve server that is not triton is not flagged", func(t *testing.T) {
|
|
body := `{"name":"my-model-server","version":"1.0","extensions":["model_repository"]}`
|
|
if res := runGPUServingModule(t, triton, 200, body); len(res.Findings) > 0 {
|
|
t.Errorf("a non-triton kserve server should not match, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("a triton name without extensions is not flagged", func(t *testing.T) {
|
|
if res := runGPUServingModule(t, triton, 200, `{"name":"triton"}`); len(res.Findings) > 0 {
|
|
t.Errorf("a name-only body should not match triton, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
|
for _, file := range []string{triton, sglang, torchserve} {
|
|
if res := runGPUServingModule(t, file, 200, "ok"); len(res.Findings) > 0 {
|
|
t.Errorf("%s: a plain 200 body should not match, got %d findings", file, len(res.Findings))
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("a 404 is not a leak", func(t *testing.T) {
|
|
for _, file := range []string{triton, sglang, torchserve} {
|
|
if res := runGPUServingModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
|
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
|
}
|
|
}
|
|
})
|
|
|
|
}
|