Convert hand-rolled inline expectations test

This commit is contained in:
Owen Mansel-Chan
2026-06-10 07:07:49 +02:00
parent f5919875b7
commit 4c411bbcb5
7 changed files with 95 additions and 228 deletions

View File

@@ -1,2 +1,4 @@
query: Security/CWE-918/RequestForgery.ql
postprocess: utils/test/PrettyPrintModels.ql
postprocess:
- utils/test/PrettyPrintModels.ql
- utils/test/InlineExpectationsTestQuery.ql

View File

@@ -9,7 +9,7 @@ import (
)
func main() {
client := notes.NewNotesServiceProtobufClient("http://localhost:8000", &http.Client{}) // test: ssrfSink
client := notes.NewNotesServiceProtobufClient("http://localhost:8000", &http.Client{}) // $ ssrfSink
ctx := context.Background()

View File

@@ -20,7 +20,7 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Note struct { // test: message
type Note struct { // $ message
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
@@ -83,7 +83,7 @@ func (x *Note) GetCreatedAt() int64 {
return 0
}
type CreateNoteParams struct { // test: message
type CreateNoteParams struct { // $ message
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
@@ -130,7 +130,7 @@ func (x *CreateNoteParams) GetText() string {
return ""
}
type GetAllNotesParams struct { // test: message
type GetAllNotesParams struct { // $ message
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
@@ -168,7 +168,7 @@ func (*GetAllNotesParams) Descriptor() ([]byte, []int) {
return file_rpc_notes_service_proto_rawDescGZIP(), []int{2}
}
type GetAllNotesResult struct { // test: message
type GetAllNotesResult struct { // $ message
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
@@ -340,7 +340,7 @@ func file_rpc_notes_service_proto_init() {
}
}
}
type x struct{}
type x struct{} // $ SPURIOUS: message // not message
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),

View File

@@ -31,7 +31,7 @@ const _ = twirp.TwirpPackageMinVersion_8_1_0
// NotesService Interface
// ======================
type NotesService interface { // test: serviceInterface
type NotesService interface { // $ serviceInterface
CreateNote(context.Context, *CreateNoteParams) (*Note, error)
GetAllNotes(context.Context, *GetAllNotesParams) (*GetAllNotesResult, error)
@@ -41,7 +41,7 @@ type NotesService interface { // test: serviceInterface
// NotesService Protobuf Client
// ============================
type notesServiceProtobufClient struct { // test: serviceClient
type notesServiceProtobufClient struct { // $ serviceClient
client HTTPClient
urls [2]string
interceptor twirp.Interceptor
@@ -50,7 +50,7 @@ type notesServiceProtobufClient struct { // test: serviceClient
// NewNotesServiceProtobufClient creates a Protobuf client that implements the NotesService interface.
// It communicates using Protobuf and can be configured with a custom HTTPClient.
func NewNotesServiceProtobufClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) NotesService { // test: clientConstructor
func NewNotesServiceProtobufClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) NotesService { // $ clientConstructor
if c, ok := client.(*http.Client); ok {
client = withoutRedirects(c)
}
@@ -84,7 +84,7 @@ func NewNotesServiceProtobufClient(baseURL string, client HTTPClient, opts ...tw
}
}
func (c *notesServiceProtobufClient) CreateNote(ctx context.Context, in *CreateNoteParams) (*Note, error) { // test: !handler
func (c *notesServiceProtobufClient) CreateNote(ctx context.Context, in *CreateNoteParams) (*Note, error) { // not handler
ctx = ctxsetters.WithPackageName(ctx, "gotwirprpcexample.rpc.notes")
ctx = ctxsetters.WithServiceName(ctx, "NotesService")
ctx = ctxsetters.WithMethodName(ctx, "CreateNote")
@@ -113,7 +113,7 @@ func (c *notesServiceProtobufClient) CreateNote(ctx context.Context, in *CreateN
return caller(ctx, in)
}
func (c *notesServiceProtobufClient) callCreateNote(ctx context.Context, in *CreateNoteParams) (*Note, error) { // test: !handler
func (c *notesServiceProtobufClient) callCreateNote(ctx context.Context, in *CreateNoteParams) (*Note, error) { // not handler
out := new(Note)
ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out)
if err != nil {
@@ -130,7 +130,7 @@ func (c *notesServiceProtobufClient) callCreateNote(ctx context.Context, in *Cre
return out, nil
}
func (c *notesServiceProtobufClient) GetAllNotes(ctx context.Context, in *GetAllNotesParams) (*GetAllNotesResult, error) { // test: !handler
func (c *notesServiceProtobufClient) GetAllNotes(ctx context.Context, in *GetAllNotesParams) (*GetAllNotesResult, error) { // not handler
ctx = ctxsetters.WithPackageName(ctx, "gotwirprpcexample.rpc.notes")
ctx = ctxsetters.WithServiceName(ctx, "NotesService")
ctx = ctxsetters.WithMethodName(ctx, "GetAllNotes")
@@ -159,7 +159,7 @@ func (c *notesServiceProtobufClient) GetAllNotes(ctx context.Context, in *GetAll
return caller(ctx, in)
}
func (c *notesServiceProtobufClient) callGetAllNotes(ctx context.Context, in *GetAllNotesParams) (*GetAllNotesResult, error) { // test: !handler
func (c *notesServiceProtobufClient) callGetAllNotes(ctx context.Context, in *GetAllNotesParams) (*GetAllNotesResult, error) { // not handler
out := new(GetAllNotesResult)
ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[1], in, out)
if err != nil {
@@ -180,7 +180,7 @@ func (c *notesServiceProtobufClient) callGetAllNotes(ctx context.Context, in *Ge
// NotesService JSON Client
// ========================
type notesServiceJSONClient struct { // test: serviceClient
type notesServiceJSONClient struct { // $ serviceClient
client HTTPClient
urls [2]string
interceptor twirp.Interceptor
@@ -189,7 +189,7 @@ type notesServiceJSONClient struct { // test: serviceClient
// NewNotesServiceJSONClient creates a JSON client that implements the NotesService interface.
// It communicates using JSON and can be configured with a custom HTTPClient.
func NewNotesServiceJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) NotesService { // test: clientConstructor
func NewNotesServiceJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) NotesService { // $ clientConstructor
if c, ok := client.(*http.Client); ok {
client = withoutRedirects(c)
}
@@ -223,7 +223,7 @@ func NewNotesServiceJSONClient(baseURL string, client HTTPClient, opts ...twirp.
}
}
func (c *notesServiceJSONClient) CreateNote(ctx context.Context, in *CreateNoteParams) (*Note, error) { // test: !handler
func (c *notesServiceJSONClient) CreateNote(ctx context.Context, in *CreateNoteParams) (*Note, error) { // not handler
ctx = ctxsetters.WithPackageName(ctx, "gotwirprpcexample.rpc.notes")
ctx = ctxsetters.WithServiceName(ctx, "NotesService")
ctx = ctxsetters.WithMethodName(ctx, "CreateNote")
@@ -252,7 +252,7 @@ func (c *notesServiceJSONClient) CreateNote(ctx context.Context, in *CreateNoteP
return caller(ctx, in)
}
func (c *notesServiceJSONClient) callCreateNote(ctx context.Context, in *CreateNoteParams) (*Note, error) { // test: !handler
func (c *notesServiceJSONClient) callCreateNote(ctx context.Context, in *CreateNoteParams) (*Note, error) { // not handler
out := new(Note)
ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out)
if err != nil {
@@ -269,7 +269,7 @@ func (c *notesServiceJSONClient) callCreateNote(ctx context.Context, in *CreateN
return out, nil
}
func (c *notesServiceJSONClient) GetAllNotes(ctx context.Context, in *GetAllNotesParams) (*GetAllNotesResult, error) { // test: !handler
func (c *notesServiceJSONClient) GetAllNotes(ctx context.Context, in *GetAllNotesParams) (*GetAllNotesResult, error) { // not handler
ctx = ctxsetters.WithPackageName(ctx, "gotwirprpcexample.rpc.notes")
ctx = ctxsetters.WithServiceName(ctx, "NotesService")
ctx = ctxsetters.WithMethodName(ctx, "GetAllNotes")
@@ -298,7 +298,7 @@ func (c *notesServiceJSONClient) GetAllNotes(ctx context.Context, in *GetAllNote
return caller(ctx, in)
}
func (c *notesServiceJSONClient) callGetAllNotes(ctx context.Context, in *GetAllNotesParams) (*GetAllNotesResult, error) { // test: !handler
func (c *notesServiceJSONClient) callGetAllNotes(ctx context.Context, in *GetAllNotesParams) (*GetAllNotesResult, error) { // not handler
out := new(GetAllNotesResult)
ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[1], in, out)
if err != nil {
@@ -319,7 +319,7 @@ func (c *notesServiceJSONClient) callGetAllNotes(ctx context.Context, in *GetAll
// NotesService Server Handler
// ===========================
type notesServiceServer struct { // test: serviceServer
type notesServiceServer struct { // $ serviceServer
NotesService
interceptor twirp.Interceptor
hooks *twirp.ServerHooks
@@ -331,7 +331,7 @@ type notesServiceServer struct { // test: serviceServer
// NewNotesServiceServer builds a TwirpServer that can be used as an http.Handler to handle
// HTTP requests that are routed to the right method in the provided svc implementation.
// The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks).
func NewNotesServiceServer(svc NotesService, opts ...interface{}) TwirpServer { // test: serverConstructor
func NewNotesServiceServer(svc NotesService, opts ...interface{}) TwirpServer { // $ serverConstructor
serverOpts := newServerOpts(opts)
// Using ReadOpt allows backwards and forwards compatibility with new options in the future
@@ -535,7 +535,7 @@ func (s *notesServiceServer) serveCreateNoteProtobuf(ctx context.Context, resp h
return
}
buf, err := io.ReadAll(req.Body)
buf, err := io.ReadAll(req.Body) // $ Source
if err != nil {
s.handleRequestBodyError(ctx, resp, "failed to read request body", err)
return
@@ -812,7 +812,7 @@ func (s *notesServiceServer) PathPrefix() string {
// automatically disabled if *(net/http).Client is passed to client
// constructors. See the withoutRedirects function in this file for more
// details.
type HTTPClient interface {
type HTTPClient interface { // $ SPURIOUS: serviceInterface // not serviceInterface
Do(req *http.Request) (*http.Response, error)
}
@@ -820,7 +820,7 @@ type HTTPClient interface {
// HTTP handlers with additional methods for accessing metadata about the
// service. Those accessors are a low-level API for building reflection tools.
// Most people can think of TwirpServers as just http.Handlers.
type TwirpServer interface {
type TwirpServer interface { // $ SPURIOUS: serviceInterface // not serviceInterface
http.Handler
// ServiceDescriptor returns gzipped bytes describing the .proto file that

View File

@@ -16,7 +16,7 @@ type notesService struct {
CurrentId int32
}
func (s *notesService) CreateNote(ctx context.Context, params *notes.CreateNoteParams) (*notes.Note, error) { // test: routeHandler, request
func (s *notesService) CreateNote(ctx context.Context, params *notes.CreateNoteParams) (*notes.Note, error) { // $ Source request handler // route handler
if len(params.Text) < 4 {
return nil, twirp.InvalidArgument.Error("Text should be min 4 characters.")
}
@@ -27,8 +27,8 @@ func (s *notesService) CreateNote(ctx context.Context, params *notes.CreateNoteP
CreatedAt: time.Now().UnixMilli(),
}
notes.NewNotesServiceProtobufClient(params.Text, &http.Client{}) // test: ssrfSink, ssrf
notes.NewNotesServiceProtobufClient(strconv.FormatInt(int64(s.CurrentId), 10), &http.Client{}) // test: ssrfSink, !ssrf
notes.NewNotesServiceProtobufClient(params.Text, &http.Client{}) // $ Alert ssrfSink ssrf
notes.NewNotesServiceProtobufClient(strconv.FormatInt(int64(s.CurrentId), 10), &http.Client{}) // $ ssrfSink // not ssrf
s.Notes = append(s.Notes, note)
@@ -37,7 +37,7 @@ func (s *notesService) CreateNote(ctx context.Context, params *notes.CreateNoteP
return &note, nil
}
func (s *notesService) GetAllNotes(ctx context.Context, params *notes.GetAllNotesParams) (*notes.GetAllNotesResult, error) { // test: routeHandler, request
func (s *notesService) GetAllNotes(ctx context.Context, params *notes.GetAllNotesParams) (*notes.GetAllNotesResult, error) { // $ request handler // route handler
allNotes := make([]*notes.Note, 0)
fmt.Println(params)
@@ -57,7 +57,7 @@ func main() {
mux := http.NewServeMux()
mux.Handle(notesServer.PathPrefix(), notesServer)
err := http.ListenAndServe(":8000", notesServer) // test: !ssrfSink
err := http.ListenAndServe(":8000", notesServer) // not ssrfSink
if err != nil {
panic(err)
}

View File

@@ -1,32 +1,2 @@
invalidModelRow
passingPositiveTests
| PASSED | clientConstructor | rpc/notes/service.twirp.go:53:114:53:139 | comment |
| PASSED | clientConstructor | rpc/notes/service.twirp.go:192:110:192:135 | comment |
| PASSED | message | rpc/notes/service.pb.go:23:20:23:35 | comment |
| PASSED | message | rpc/notes/service.pb.go:86:32:86:47 | comment |
| PASSED | message | rpc/notes/service.pb.go:133:33:133:48 | comment |
| PASSED | message | rpc/notes/service.pb.go:171:33:171:48 | comment |
| PASSED | request | server/main.go:19:111:19:140 | comment |
| PASSED | request | server/main.go:40:126:40:155 | comment |
| PASSED | serverConstructor | rpc/notes/service.twirp.go:334:81:334:106 | comment |
| PASSED | serviceClient | rpc/notes/service.twirp.go:44:42:44:63 | comment |
| PASSED | serviceClient | rpc/notes/service.twirp.go:183:38:183:59 | comment |
| PASSED | serviceInterface | rpc/notes/service.twirp.go:34:31:34:55 | comment |
| PASSED | serviceServer | rpc/notes/service.twirp.go:322:34:322:55 | comment |
| PASSED | ssrf | server/main.go:30:97:30:119 | comment |
| PASSED | ssrfSink | client/main.go:12:89:12:105 | comment |
| PASSED | ssrfSink | server/main.go:30:97:30:119 | comment |
| PASSED | ssrfSink | server/main.go:31:97:31:120 | comment |
failingPositiveTests
passingNegativeTests
| PASSED | !handler | rpc/notes/service.twirp.go:87:109:87:125 | comment |
| PASSED | !handler | rpc/notes/service.twirp.go:116:113:116:129 | comment |
| PASSED | !handler | rpc/notes/service.twirp.go:133:124:133:140 | comment |
| PASSED | !handler | rpc/notes/service.twirp.go:162:128:162:144 | comment |
| PASSED | !handler | rpc/notes/service.twirp.go:226:105:226:121 | comment |
| PASSED | !handler | rpc/notes/service.twirp.go:255:109:255:125 | comment |
| PASSED | !handler | rpc/notes/service.twirp.go:272:120:272:136 | comment |
| PASSED | !handler | rpc/notes/service.twirp.go:301:124:301:140 | comment |
| PASSED | !ssrf | server/main.go:31:97:31:120 | comment |
| PASSED | !ssrfSink | server/main.go:60:51:60:68 | comment |
failingNegativeTests
testFailures

View File

@@ -2,181 +2,76 @@ import go
import semmle.go.dataflow.ExternalFlow
import ModelValidation
import semmle.go.security.RequestForgery
import utils.test.InlineExpectationsTest
class InlineTest extends LineComment {
string tests;
InlineTest() { tests = this.getText().regexpCapture("\\s*test:(.*)", 1) }
string getPositiveTest() {
result = tests.trim().splitAt(",").trim() and not result.matches("!%")
module TwirpTest implements TestSig {
string getARelevantTag() {
result =
[
"handler", "request", "ssrfSink", "message", "serviceInterface", "serviceClient",
"serviceServer", "clientConstructor", "serverConstructor", "ssrf"
]
}
string getNegativeTest() { result = tests.trim().splitAt(",").trim() and result.matches("!%") }
predicate hasPositiveTest(string test) { test = this.getPositiveTest() }
predicate hasNegativeTest(string test) { test = this.getNegativeTest() }
predicate inNode(DataFlow::Node n) {
this.getLocation().getFile() = n.getFile() and
this.getLocation().getStartLine() = n.getStartLine()
additional predicate hasEntityResult(Location location, string element, Entity entity) {
location = entity.getDeclaration().getLocation() and
element = entity.toString()
}
predicate inEntity(Entity e) {
this.getLocation().getFile() = e.getDeclaration().getFile() and
this.getLocation().getStartLine() = e.getDeclaration().getLocation().getStartLine()
additional predicate hasTypeResult(Location location, string element, Type goType) {
exists(TypeEntity typeEntity |
typeEntity.getType() = goType and
location = typeEntity.getDeclaration().getLocation() and
element = goType.toString()
)
}
predicate inType(Type t) {
exists(TypeEntity te |
te.getType() = t and
this.getLocation().getFile() = te.getDeclaration().getFile() and
this.getLocation().getStartLine() = te.getDeclaration().getLocation().getStartLine()
predicate hasActualResult(Location location, string element, string tag, string value) {
value = "" and
(
tag = "handler" and
exists(Twirp::ServiceHandler handler | hasEntityResult(location, element, handler))
or
tag = "request" and
exists(Twirp::Request request |
location = request.getLocation() and
element = request.toString()
)
or
tag = "ssrfSink" and
exists(RequestForgery::Sink sink |
location = sink.getLocation() and
element = sink.toString()
)
or
tag = "message" and
exists(Twirp::ProtobufMessageType message | hasTypeResult(location, element, message))
or
tag = "serviceInterface" and
exists(Twirp::ServiceInterfaceType serviceInterface |
hasTypeResult(location, element, serviceInterface.getDefinedType())
)
or
tag = "serviceClient" and
exists(Twirp::ServiceClientType client | hasTypeResult(location, element, client))
or
tag = "serviceServer" and
exists(Twirp::ServiceServerType server | hasTypeResult(location, element, server))
or
tag = "clientConstructor" and
exists(Twirp::ClientConstructor constructor | hasEntityResult(location, element, constructor))
or
tag = "serverConstructor" and
exists(Twirp::ServerConstructor constructor | hasEntityResult(location, element, constructor))
or
tag = "ssrf" and
exists(DataFlow::Node sink |
RequestForgery::Flow::flowTo(sink) and
location = sink.getLocation() and
element = sink.toString()
)
)
}
}
query predicate passingPositiveTests(string res, string expectation, InlineTest t) {
res = "PASSED" and
t.hasPositiveTest(expectation) and
(
expectation = "handler" and
exists(Twirp::ServiceHandler n | t.inEntity(n))
or
expectation = "request" and
exists(Twirp::Request n | t.inNode(n))
or
expectation = "ssrfSink" and
exists(RequestForgery::Sink n | t.inNode(n))
or
expectation = "message" and
exists(Twirp::ProtobufMessageType n | t.inType(n))
or
expectation = "serviceInterface" and
exists(Twirp::ServiceInterfaceType n | t.inType(n.getDefinedType()))
or
expectation = "serviceClient" and
exists(Twirp::ServiceClientType n | t.inType(n))
or
expectation = "serviceServer" and
exists(Twirp::ServiceServerType n | t.inType(n))
or
expectation = "clientConstructor" and
exists(Twirp::ClientConstructor n | t.inEntity(n))
or
expectation = "serverConstructor" and
exists(Twirp::ServerConstructor n | t.inEntity(n))
or
expectation = "ssrf" and
exists(DataFlow::Node sink | RequestForgery::Flow::flowTo(sink) and t.inNode(sink))
)
}
query predicate failingPositiveTests(string res, string expectation, InlineTest t) {
res = "FAILED" and
t.hasPositiveTest(expectation) and
(
expectation = "handler" and
not exists(Twirp::ServiceHandler n | t.inEntity(n))
or
expectation = "request" and
not exists(Twirp::Request n | t.inNode(n))
or
expectation = "ssrfSink" and
not exists(RequestForgery::Sink n | t.inNode(n))
or
expectation = "message" and
not exists(Twirp::ProtobufMessageType n | t.inType(n))
or
expectation = "serviceInterface" and
not exists(Twirp::ServiceInterfaceType n | t.inType(n.getDefinedType()))
or
expectation = "serviceClient" and
not exists(Twirp::ServiceClientType n | t.inType(n))
or
expectation = "serviceServer" and
not exists(Twirp::ServiceServerType n | t.inType(n))
or
expectation = "clientConstructor" and
not exists(Twirp::ClientConstructor n | t.inEntity(n))
or
expectation = "serverConstructor" and
not exists(Twirp::ServerConstructor n | t.inEntity(n))
or
expectation = "ssrf" and
not exists(DataFlow::Node sink | RequestForgery::Flow::flowTo(sink) and t.inNode(sink))
)
}
query predicate passingNegativeTests(string res, string expectation, InlineTest t) {
res = "PASSED" and
t.hasNegativeTest(expectation) and
(
expectation = "!handler" and
not exists(Twirp::ServiceHandler n | t.inEntity(n))
or
expectation = "!request" and
not exists(Twirp::Request n | t.inNode(n))
or
expectation = "!ssrfSink" and
not exists(RequestForgery::Sink n | t.inNode(n))
or
expectation = "!message" and
not exists(Twirp::ProtobufMessageType n | t.inType(n))
or
expectation = "!serviceInterface" and
not exists(Twirp::ServiceInterfaceType n | t.inType(n))
or
expectation = "!serviceClient" and
not exists(Twirp::ServiceClientType n | t.inType(n))
or
expectation = "!serviceServer" and
not exists(Twirp::ServiceServerType n | t.inType(n))
or
expectation = "!clientConstructor" and
not exists(Twirp::ClientConstructor n | t.inEntity(n))
or
expectation = "!serverConstructor" and
not exists(Twirp::ServerConstructor n | t.inEntity(n))
or
expectation = "!ssrf" and
not exists(DataFlow::Node sink | RequestForgery::Flow::flowTo(sink) and t.inNode(sink))
)
}
query predicate failingNegativeTests(string res, string expectation, InlineTest t) {
res = "FAILED" and
t.hasNegativeTest(expectation) and
(
expectation = "!handler" and
exists(Twirp::ServiceHandler n | t.inEntity(n))
or
expectation = "!request" and
exists(Twirp::Request n | t.inNode(n))
or
expectation = "!ssrfSink" and
exists(RequestForgery::Sink n | t.inNode(n))
or
expectation = "!message" and
exists(Twirp::ProtobufMessageType n | t.inType(n))
or
expectation = "!serviceInterface" and
exists(Twirp::ServiceInterfaceType n | t.inType(n))
or
expectation = "!serviceClient" and
exists(Twirp::ServiceClientType n | t.inType(n))
or
expectation = "!serviceServer" and
exists(Twirp::ServiceServerType n | t.inType(n))
or
expectation = "!clientConstructor" and
exists(Twirp::ClientConstructor n | t.inEntity(n))
or
expectation = "!serverConstructor" and
exists(Twirp::ServerConstructor n | t.inEntity(n))
or
expectation = "!ssrf" and
exists(DataFlow::Node sink | RequestForgery::Flow::flowTo(sink) and t.inNode(sink))
)
}
import MakeTest<TwirpTest>