Skip to content

Commit 217df73

Browse files
tooryxcopybara-github
authored andcommitted
Add the environment package for the templated format.
The environment package keeps track of variables defined throughout the lifecycle of a templated detector. PiperOrigin-RevId: 870879121
1 parent 2066971 commit 217df73

File tree

8 files changed

+518
-14
lines changed

8 files changed

+518
-14
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// Package environment provides the environment used by the templated engine to store and manage variables.
18+
package environment
19+
20+
import (
21+
"context"
22+
"fmt"
23+
"regexp"
24+
"strings"
25+
"time"
26+
27+
"github.com/google/goonami-scanner/core/log"
28+
"github.com/google/goonami-scanner/core/net/netservice"
29+
30+
nspb "github.com/google/tsunami-security-scanner/proto/go/network_service_go_proto"
31+
)
32+
33+
var variablePattern = regexp.MustCompile(`\{\{ ([a-zA-Z0-9_]+) \}\}`)
34+
35+
// Environment stores variables that are used by the templated detector.
36+
type Environment struct {
37+
vars map[string]string
38+
}
39+
40+
// New creates a new environment.
41+
func New() *Environment {
42+
return &Environment{
43+
vars: make(map[string]string),
44+
}
45+
}
46+
47+
// InitializeFor initializes the environment for a specific network service.
48+
func (e *Environment) InitializeFor(ctx context.Context, service *nspb.NetworkService) {
49+
e.Set("T_UTL_CURRENT_TIMESTAMP_MS", fmt.Sprintf("%d", time.Now().UnixMilli()))
50+
51+
webRoot, err := netservice.BuildWebRoot(service)
52+
if err == nil {
53+
e.Set("T_NS_BASEURL", webRoot)
54+
}
55+
56+
e.Set("T_NS_PROTOCOL", strings.TrimSpace(service.GetTransportProtocol().String()))
57+
58+
endpoint := service.GetNetworkEndpoint()
59+
e.Set("T_NS_HOSTNAME", strings.TrimSpace(endpoint.GetHostname().GetName()))
60+
e.Set("T_NS_PORT", fmt.Sprintf("%d", endpoint.GetPort().GetPortNumber()))
61+
e.Set("T_NS_IP", strings.TrimSpace(endpoint.GetIpAddress().GetAddress()))
62+
63+
// TODO: b/483970797 - Add callback server variables when implemented in Goonami.
64+
65+
for k, v := range e.vars {
66+
log.DebugContextf(ctx, log.DebugLevelRequest, "environment: %s = %s", k, v)
67+
}
68+
}
69+
70+
// Set sets a variable in the environment.
71+
func (e *Environment) Set(key, value string) {
72+
e.vars[key] = value
73+
}
74+
75+
// Get gets a variable from the environment.
76+
func (e *Environment) Get(key string) (string, bool) {
77+
v, ok := e.vars[key]
78+
return v, ok
79+
}
80+
81+
// Substitute replaces variables in a template string.
82+
func (e *Environment) Substitute(ctx context.Context, template string) string {
83+
return variablePattern.ReplaceAllStringFunc(template, func(match string) string {
84+
varName := variablePattern.FindStringSubmatch(match)[1]
85+
if val, ok := e.vars[varName]; ok {
86+
return val
87+
}
88+
89+
log.WarnContextf(ctx, "substitution not found for '%s' in environment", varName)
90+
return match
91+
})
92+
}
93+
94+
// Extract performs regexp extraction of pattern in content and stores it in varname.
95+
func (e *Environment) Extract(ctx context.Context, content, varname, pattern string) bool {
96+
re, err := regexp.Compile(pattern)
97+
if err != nil {
98+
log.WarnContextf(ctx, "failed to compile regexp '%s': %v", pattern, err)
99+
return false
100+
}
101+
102+
matches := re.FindStringSubmatch(content)
103+
if len(matches) < 2 {
104+
log.DebugContextf(ctx, log.DebugLevelRequest, "failed to extract variable '%s' from content using pattern '%s'", varname, pattern)
105+
return false
106+
}
107+
108+
e.vars[varname] = matches[1]
109+
return true
110+
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package environment
18+
19+
import (
20+
"context"
21+
"regexp"
22+
"testing"
23+
24+
npb "github.com/google/tsunami-security-scanner/proto/go/network_go_proto"
25+
nspb "github.com/google/tsunami-security-scanner/proto/go/network_service_go_proto"
26+
)
27+
28+
func TestEnvironment_InitializeFor(t *testing.T) {
29+
service := nspb.NetworkService_builder{
30+
NetworkEndpoint: npb.NetworkEndpoint_builder{
31+
Hostname: npb.Hostname_builder{Name: "example.com"}.Build(),
32+
Port: npb.Port_builder{PortNumber: 80}.Build(),
33+
IpAddress: npb.IpAddress_builder{
34+
Address: "1.2.3.4",
35+
}.Build(),
36+
}.Build(),
37+
TransportProtocol: npb.TransportProtocol_TCP,
38+
SupportedHttpMethods: []string{"GET"},
39+
}.Build()
40+
41+
env := New()
42+
env.InitializeFor(context.Background(), service)
43+
44+
tests := []struct {
45+
key string
46+
wantVal string
47+
wantExpr *regexp.Regexp
48+
}{
49+
{key: "T_NS_HOSTNAME", wantVal: "example.com"},
50+
{key: "T_NS_PORT", wantVal: "80"},
51+
{key: "T_NS_IP", wantVal: "1.2.3.4"},
52+
{key: "T_NS_PROTOCOL", wantVal: "TCP"},
53+
{key: "T_NS_BASEURL", wantVal: "http://example.com:80"},
54+
{key: "T_UTL_CURRENT_TIMESTAMP_MS", wantExpr: regexp.MustCompile(`^\d+$`)},
55+
}
56+
57+
for _, tc := range tests {
58+
got, ok := env.Get(tc.key)
59+
if !ok {
60+
t.Errorf("env.Get(%q) = _, false, want _, true", tc.key)
61+
continue
62+
}
63+
if tc.wantVal != "" && got != tc.wantVal {
64+
t.Errorf("env.Get(%q) = %q, want %q", tc.key, got, tc.wantVal)
65+
}
66+
if tc.wantExpr != nil && !tc.wantExpr.MatchString(got) {
67+
t.Errorf("env.Get(%q) = %q, doesn't match %v", tc.key, got, tc.wantExpr)
68+
}
69+
}
70+
}
71+
72+
func TestEnvironment_Substitute(t *testing.T) {
73+
env := New()
74+
env.Set("var1", "val1")
75+
env.Set("var2", "val2")
76+
77+
tests := []struct {
78+
name string
79+
template string
80+
want string
81+
}{
82+
{
83+
name: "when_no_variables_returns_original",
84+
template: "hello world",
85+
want: "hello world",
86+
},
87+
{
88+
name: "when_single_variable_replaces_it",
89+
template: "hello {{ var1 }}",
90+
want: "hello val1",
91+
},
92+
{
93+
name: "when_multiple_variables_replaces_them",
94+
template: "{{ var1 }} and {{ var2 }}",
95+
want: "val1 and val2",
96+
},
97+
{
98+
name: "when_repeated_variable_replaces_all",
99+
template: "hello {{ var1 }} {{ var1 }}",
100+
want: "hello val1 val1",
101+
},
102+
{
103+
name: "when_variable_not_found_leaves_it",
104+
template: "hello {{ unknown }}",
105+
want: "hello {{ unknown }}",
106+
},
107+
{
108+
name: "when_malformed_variable_leaves_it",
109+
template: "hello {{var1}}",
110+
want: "hello {{var1}}",
111+
},
112+
}
113+
114+
for _, tc := range tests {
115+
t.Run(tc.name, func(t *testing.T) {
116+
got := env.Substitute(context.Background(), tc.template)
117+
if got != tc.want {
118+
t.Errorf("Substitute(%q) = %q, want %q", tc.template, got, tc.want)
119+
}
120+
})
121+
}
122+
}
123+
124+
func TestEnvironment_Extract(t *testing.T) {
125+
tests := []struct {
126+
name string
127+
content string
128+
pattern string
129+
varname string
130+
wantOk bool
131+
wantVars map[string]string
132+
}{
133+
{
134+
name: "when_match_found_extracts_it",
135+
content: "token: abc-123",
136+
pattern: `token: ([a-z0-9-]+)`,
137+
varname: "token",
138+
wantOk: true,
139+
wantVars: map[string]string{
140+
"token": "abc-123",
141+
},
142+
},
143+
{
144+
name: "when_no_match_found_returns_false",
145+
content: "hello world",
146+
pattern: `token: ([a-z0-9-]+)`,
147+
varname: "token",
148+
wantOk: false,
149+
wantVars: map[string]string{
150+
"token": "",
151+
},
152+
},
153+
{
154+
name: "when_invalid_regexp_returns_false",
155+
content: "hello world",
156+
pattern: `(`,
157+
varname: "token",
158+
wantOk: false,
159+
wantVars: map[string]string{
160+
"token": "",
161+
},
162+
},
163+
}
164+
165+
for _, tc := range tests {
166+
t.Run(tc.name, func(t *testing.T) {
167+
env := New()
168+
gotOk := env.Extract(context.Background(), tc.content, tc.varname, tc.pattern)
169+
if gotOk != tc.wantOk {
170+
t.Errorf("Extract() = %v, want %v", gotOk, tc.wantOk)
171+
}
172+
173+
for k, v := range tc.wantVars {
174+
val, ok := env.Get(k)
175+
if v == "" {
176+
if ok {
177+
t.Errorf("env.Get(%q) = %q, true, want _, false", k, val)
178+
}
179+
} else {
180+
if !ok || val != v {
181+
t.Errorf("env.Get(%q) = %q, %v, want %q, true", k, val, ok, v)
182+
}
183+
}
184+
}
185+
})
186+
}
187+
}
188+
189+
func TestEnvironment_SetGet(t *testing.T) {
190+
env := New()
191+
env.Set("key", "value")
192+
val, ok := env.Get("key")
193+
if !ok || val != "value" {
194+
t.Errorf("Get() = %q, %v, want %q, true", val, ok, "value")
195+
}
196+
197+
_, ok = env.Get("unknown")
198+
if ok {
199+
t.Errorf("Get(unknown) = _, true, want _, false")
200+
}
201+
}

core/log/color.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package log
18+
19+
const (
20+
ansiReset = "\033[0m"
21+
ansiBoldWhite = "\033[1;37m"
22+
ansiBoldBlue = "\033[1;34m"
23+
ansiBoldYellow = "\033[1;33m"
24+
ansiBoldRed = "\033[1;31m"
25+
ansiBoldGray = "\033[1;30m"
26+
ansiBoldGreen = "\033[1;32m"
27+
ansiGreen = "\033[0;32m"
28+
ansiBoldCyan = "\033[1;36m"
29+
ansiCyan = "\033[0;36m"
30+
)
31+
32+
// colorize applies the given ANSI color to the string if enabled is true.
33+
func colorize(s string, color string, enabled bool) string {
34+
if !enabled {
35+
return s
36+
}
37+
38+
return color + s + ansiReset
39+
}

0 commit comments

Comments
 (0)