Skip to content

Commit 00e9b19

Browse files
Merge pull request #115 from step-security/feature-28
Update cache to use TTL
2 parents 6778f21 + d5f3859 commit 00e9b19

16 files changed

+299
-117
lines changed

.github/dependabot.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# To get started with Dependabot version updates, you'll need to specify which
2+
# package ecosystems to update and where the package manifests are located.
3+
# Please see the documentation for all configuration options:
4+
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5+
6+
version: 2
7+
updates:
8+
- package-ecosystem: "gomod" # See documentation for possible values
9+
directory: "/" # Location of package manifests
10+
schedule:
11+
interval: "daily"
12+
13+
- package-ecosystem: "github-actions"
14+
directory: "/"
15+
schedule:
16+
interval: "daily"

.github/workflows/codeql-analysis.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545

4646
# Initializes the CodeQL tools for scanning.
4747
- name: Initialize CodeQL
48-
uses: github/codeql-action/init@e095058bfa09de8070f94e98f5dc059531bc6235
48+
uses: github/codeql-action/init@5f532563584d71fdef14ee64d17bafb34f751ce5
4949
with:
5050
languages: ${{ matrix.language }}
5151
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -56,7 +56,7 @@ jobs:
5656
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
5757
# If this step fails, then you should remove it and run the build manually (see below)
5858
- name: Autobuild
59-
uses: github/codeql-action/autobuild@e095058bfa09de8070f94e98f5dc059531bc6235
59+
uses: github/codeql-action/autobuild@5f532563584d71fdef14ee64d17bafb34f751ce5
6060

6161
# ℹ️ Command-line programs to run using the OS shell.
6262
# 📚 https://git.io/JvXDl
@@ -70,4 +70,4 @@ jobs:
7070
# make release
7171

7272
- name: Perform CodeQL Analysis
73-
uses: github/codeql-action/analyze@e095058bfa09de8070f94e98f5dc059531bc6235
73+
uses: github/codeql-action/analyze@5f532563584d71fdef14ee64d17bafb34f751ce5

.github/workflows/int.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
- name: Checkout
2828
uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5
2929
- name: Set up Go
30-
uses: actions/setup-go@37335c7bb261b353407cff977110895fa0b4f7d8
30+
uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4
3131
with:
3232
go-version: 1.17
3333
- run: sudo go test -v

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
- name: Checkout
2727
uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5
2828
- name: Set up Go
29-
uses: actions/setup-go@37335c7bb261b353407cff977110895fa0b4f7d8
29+
uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4
3030
with:
3131
go-version: 1.17
3232
- uses: goreleaser/goreleaser-action@5df302e5e9e4c66310a6b6493a8865b12c555af2

.github/workflows/scorecard-analysis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,6 @@ jobs:
4747
retention-days: 5
4848

4949
- name: "Upload SARIF results"
50-
uses: github/codeql-action/upload-sarif@e095058bfa09de8070f94e98f5dc059531bc6235
50+
uses: github/codeql-action/upload-sarif@5f532563584d71fdef14ee64d17bafb34f751ce5
5151
with:
5252
sarif_file: results.sarif

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
- name: Checkout
1414
uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5
1515
- name: Set up Go
16-
uses: actions/setup-go@37335c7bb261b353407cff977110895fa0b4f7d8
16+
uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4
1717
with:
1818
go-version: 1.17
1919
- name: Run coverage

agent.go

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"net/http"
77
"os"
8+
"time"
89

910
"github.com/florianl/go-nflog/v2"
1011
)
@@ -42,6 +43,8 @@ type Firewall struct {
4243

4344
type IPTables interface {
4445
Append(table, chain string, rulespec ...string) error
46+
Insert(table, chain string, pos int, rulespec ...string) error
47+
Exists(table, chain string, rulespec ...string) (bool, error)
4548
ClearChain(table, chain string) error
4649
}
4750

@@ -67,8 +70,7 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer,
6770

6871
WriteLog(fmt.Sprintf("%s %s", StepSecurityLogCorrelationPrefix, config.CorrelationId))
6972

70-
// TODO: fix the cache and time
71-
Cache := InitCache(10 * 60 * 1000000000) // 10 * 60 seconds
73+
Cache := InitCache(config.EgressPolicy)
7274

7375
allowedEndpoints := addImplicitEndpoints(config.Endpoints)
7476

@@ -98,17 +100,19 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer,
98100

99101
// hydrate dns cache
100102
if config.EgressPolicy == EgressPolicyBlock {
101-
for _, endpoint := range allowedEndpoints {
103+
for domainName, endpoints := range allowedEndpoints {
102104
// this will cause domain, IP mapping to be cached
103-
ipAddress, err := dnsProxy.getIPByDomain(endpoint.domainName)
105+
ipAddress, err := dnsProxy.getIPByDomain(domainName)
104106
if err != nil {
105107
WriteLog(fmt.Sprintf("Error resolving allowed domain %v", err))
106108
RevertChanges(iptables, nflog, cmd, resolvdConfigPath, dockerDaemonConfigPath, dnsConfig)
107109
return err
108110
}
111+
for _, endpoint := range endpoints {
112+
// create list of ip address to be added to firewall
113+
ipAddressEndpoints = append(ipAddressEndpoints, ipAddressEndpoint{ipAddress: ipAddress, port: fmt.Sprintf("%d", endpoint.port)})
114+
}
109115

110-
// create list of ip address to be added to firewall
111-
ipAddressEndpoints = append(ipAddressEndpoints, ipAddressEndpoint{ipAddress: ipAddress, port: fmt.Sprintf("%d", endpoint.port)})
112116
}
113117
}
114118

@@ -165,11 +169,13 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer,
165169
// Start network monitor
166170
go netMonitor.MonitorNetwork(nflog, errc) // listens for NFLOG messages
167171

168-
if err := addBlockRulesForGitHubHostedRunner(ipAddressEndpoints); err != nil {
172+
if err := addBlockRulesForGitHubHostedRunner(iptables, ipAddressEndpoints); err != nil {
169173
WriteLog(fmt.Sprintf("Error setting firewall for allowed domains %v", err))
170174
RevertChanges(iptables, nflog, cmd, resolvdConfigPath, dockerDaemonConfigPath, dnsConfig)
171175
return err
172176
}
177+
178+
go refreshDNSEntries(ctx, iptables, allowedEndpoints, &dnsProxy)
173179
}
174180

175181
WriteLog("done")
@@ -190,7 +196,60 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer,
190196
}
191197
}
192198

193-
func addImplicitEndpoints(endpoints []Endpoint) []Endpoint {
199+
func refreshDNSEntries(ctx context.Context, iptables *Firewall, allowedEndpoints map[string][]Endpoint, dnsProxy *DNSProxy) {
200+
ticker := time.NewTicker(30 * time.Second)
201+
go func() {
202+
for {
203+
select {
204+
case <-ctx.Done():
205+
return
206+
case <-ticker.C:
207+
WriteLog("Refreshing DNS entries")
208+
for domainName, endpoints := range allowedEndpoints {
209+
element, found := dnsProxy.Cache.Get(domainName)
210+
if found {
211+
var err error
212+
var answer *Answer
213+
// check if DNS TTL is close to expiry
214+
if time.Now().Unix()+10-element.TimeAdded > int64(element.Value.TTL) {
215+
// resolve domain name
216+
answer, err = dnsProxy.ResolveDomain(domainName)
217+
if err != nil {
218+
// log and continue
219+
WriteLog(fmt.Sprintf("domain could not be resolved: %s, %v", domainName, err))
220+
continue
221+
}
222+
223+
for _, endpoint := range endpoints {
224+
// add endpoint to firewall
225+
err = InsertAllowRule(iptables, answer.Data, fmt.Sprintf("%d", endpoint.port))
226+
if err != nil {
227+
break
228+
}
229+
}
230+
231+
if err != nil {
232+
// log and continue
233+
WriteLog(fmt.Sprintf("failed to insert new ipaddress in firewall: %s, %v", domainName, err))
234+
continue
235+
}
236+
237+
// add to cache with new TTL
238+
dnsProxy.Cache.Set(domainName, answer)
239+
240+
go dnsProxy.ApiClient.sendDNSRecord(dnsProxy.CorrelationId, dnsProxy.Repo, domainName, answer.Data)
241+
242+
WriteLog(fmt.Sprintf("domain resolved: %s, ip address: %s, TTL: %d", domainName, answer.Data, answer.TTL))
243+
}
244+
}
245+
}
246+
}
247+
}
248+
}()
249+
250+
}
251+
252+
func addImplicitEndpoints(endpoints map[string][]Endpoint) map[string][]Endpoint {
194253
implicitEndpoints := []Endpoint{
195254
{domainName: "agent.api.stepsecurity.io", port: 443}, // Should be implicit based on user feedback
196255
{domainName: "pipelines.actions.githubusercontent.com", port: 443}, // GitHub
@@ -200,7 +259,11 @@ func addImplicitEndpoints(endpoints []Endpoint) []Endpoint {
200259
{domainName: "vstsmms.actions.githubusercontent.com", port: 443}, // GitHub
201260
}
202261

203-
return append(endpoints, implicitEndpoints...)
262+
for _, endpoint := range implicitEndpoints {
263+
endpoints[endpoint.domainName] = append(endpoints[endpoint.domainName], endpoint)
264+
}
265+
266+
return endpoints
204267
}
205268

206269
func RevertChanges(iptables *Firewall, nflog AgentNflogger,

agent_test.go

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
"github.com/florianl/go-nflog/v2"
12+
"github.com/jarcoal/httpmock"
1213
)
1314

1415
type mockDNSServer struct {
@@ -36,6 +37,14 @@ func (m *MockIPTables) Append(table, chain string, rulespec ...string) error {
3637
return nil
3738
}
3839

40+
func (m *MockIPTables) Exists(table, chain string, rulespec ...string) (bool, error) {
41+
return false, nil
42+
}
43+
44+
func (m *MockIPTables) Insert(table, chain string, post int, rulespec ...string) error {
45+
return nil
46+
}
47+
3948
func (m *MockIPTables) ClearChain(table, chain string) error {
4049
return nil
4150
}
@@ -83,39 +92,14 @@ func (m *MockCommandWithError) Run() error {
8392
return fmt.Errorf("failed to run command")
8493
}
8594

86-
/*
87-
func TestRunWithNflogError(t *testing.T) {
88-
89-
ctx := context.Background()
90-
ctx, cancel := context.WithCancel(ctx)
91-
time.AfterFunc(5*time.Second, cancel) // this should not be used, it should error out earlier
92-
93-
httpmock.Activate()
94-
defer httpmock.DeactivateAndReset()
95-
96-
httpmock.RegisterResponder("POST", fmt.Sprintf("%s/owner/repo/actions/runs/1287185438/monitor", agentApiBaseUrl),
97-
httpmock.NewStringResponder(200, ""))
98-
99-
err := Run(ctx, "./testfiles/agent.json",
100-
&mockDNSServer{}, &mockDNSServer{}, &Firewall{&MockIPTables{}},
101-
&MockAgentNfloggerWithErr{}, &MockCommand{}, createTempFileWithContents(""), createTempFileWithContents("{}"), nil)
102-
103-
// if 2 seconds pass
104-
if err == nil {
105-
t.Fail()
106-
}
107-
108-
}
109-
*/
110-
11195
func deleteTempFile(path string) {
11296
os.Remove(path)
11397
}
11498

11599
func getContext(seconds int) context.Context {
116100
ctx := context.Background()
117101
ctx, cancel := context.WithCancel(ctx)
118-
time.AfterFunc(2*time.Second, cancel)
102+
time.AfterFunc(time.Duration(seconds)*time.Second, cancel)
119103

120104
return ctx
121105
}
@@ -134,6 +118,17 @@ func TestRun(t *testing.T) {
134118
ciTestOnly bool
135119
}
136120

121+
httpmock.Activate()
122+
123+
httpmock.RegisterResponder("GET", "https://dns.google/resolve?name=domain1.com.&type=a",
124+
httpmock.NewStringResponder(200, `{"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false,"Question":[{"name":"domain1.com.","type":1}],"Answer":[{"name":"domain1.com.","type":1,"TTL":30,"data":"67.67.67.67"}]}`))
125+
126+
httpmock.RegisterResponder("GET", "https://dns.google/resolve?name=domain2.com.&type=a",
127+
httpmock.NewStringResponder(200, `{"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false,"Question":[{"name":"domain2.com.","type":1}],"Answer":[{"name":"domain2.com.","type":1,"TTL":30,"data":"68.68.68.68"}]}`))
128+
129+
httpmock.RegisterResponder("GET", "https://dns.google/resolve", // no query params to match all other requests
130+
httpmock.NewStringResponder(200, `{"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false,"Question":[{"name":"requesteddomain.com.","type":1}],"Answer":[{"name":"requesteddomain.com.","type":1,"TTL":300,"data":"69.69.69.69"}]}`))
131+
137132
tests := []struct {
138133
name string
139134
args args
@@ -142,19 +137,34 @@ func TestRun(t *testing.T) {
142137
{name: "success", args: args{ctxCancelDuration: 2, configFilePath: "./testfiles/agent.json", hostDNSServer: &mockDNSServer{}, dockerDNSServer: &mockDNSServer{},
143138
iptables: &Firewall{&MockIPTables{}}, nflog: &MockAgentNflogger{}, cmd: &MockCommand{}, resolvdConfigPath: createTempFileWithContents(""),
144139
dockerDaemonConfigPath: createTempFileWithContents("{}")}, wantErr: false},
140+
145141
{name: "success monitor process", args: args{ctxCancelDuration: 2, configFilePath: "./testfiles/agent.json", hostDNSServer: &mockDNSServer{}, dockerDNSServer: &mockDNSServer{},
146142
iptables: &Firewall{&MockIPTables{}}, nflog: &MockAgentNflogger{}, cmd: nil, resolvdConfigPath: createTempFileWithContents(""),
147143
dockerDaemonConfigPath: createTempFileWithContents("{}"), ciTestOnly: true}, wantErr: false},
144+
148145
{name: "success allowed endpoints", args: args{ctxCancelDuration: 2, configFilePath: "./testfiles/agent-allowed-endpoints.json",
146+
hostDNSServer: &mockDNSServer{}, dockerDNSServer: &mockDNSServer{},
147+
iptables: &Firewall{&MockIPTables{}}, nflog: &MockAgentNflogger{}, cmd: &MockCommand{}, resolvdConfigPath: createTempFileWithContents(""),
148+
dockerDaemonConfigPath: createTempFileWithContents("{}")}, wantErr: false},
149+
150+
{name: "success allowed endpoints CI Test", args: args{ctxCancelDuration: 2, configFilePath: "./testfiles/agent-allowed-endpoints.json",
151+
hostDNSServer: &mockDNSServer{}, dockerDNSServer: &mockDNSServer{},
152+
iptables: nil, nflog: &MockAgentNflogger{}, cmd: &MockCommand{}, resolvdConfigPath: createTempFileWithContents(""),
153+
dockerDaemonConfigPath: createTempFileWithContents("{}"), ciTestOnly: true}, wantErr: false},
154+
155+
{name: "success allowed endpoints DNS refresh CI Test", args: args{ctxCancelDuration: 60, configFilePath: "./testfiles/agent-allowed-endpoints.json",
149156
hostDNSServer: &mockDNSServer{}, dockerDNSServer: &mockDNSServer{},
150157
iptables: nil, nflog: &MockAgentNflogger{}, cmd: &MockCommand{}, resolvdConfigPath: createTempFileWithContents(""),
151158
dockerDaemonConfigPath: createTempFileWithContents("{}"), ciTestOnly: true}, wantErr: false},
159+
152160
{name: "dns failure", args: args{ctxCancelDuration: 5, configFilePath: "./testfiles/agent.json", hostDNSServer: &mockDNSServer{}, dockerDNSServer: &mockDNSServerWithError{},
153161
iptables: &Firewall{&MockIPTables{}}, nflog: &MockAgentNflogger{}, cmd: &MockCommand{}, resolvdConfigPath: createTempFileWithContents(""),
154162
dockerDaemonConfigPath: createTempFileWithContents("{}")}, wantErr: true},
163+
155164
{name: "cmd failure", args: args{ctxCancelDuration: 5, configFilePath: "./testfiles/agent.json", hostDNSServer: &mockDNSServer{}, dockerDNSServer: &mockDNSServer{},
156165
iptables: &Firewall{&MockIPTables{}}, nflog: &MockAgentNflogger{}, cmd: &MockCommandWithError{}, resolvdConfigPath: createTempFileWithContents(""),
157166
dockerDaemonConfigPath: createTempFileWithContents("{}")}, wantErr: true},
167+
158168
{name: "nflog failure", args: args{ctxCancelDuration: 5, configFilePath: "./testfiles/agent.json", hostDNSServer: &mockDNSServer{}, dockerDNSServer: &mockDNSServer{},
159169
iptables: &Firewall{&MockIPTables{}}, nflog: &MockAgentNfloggerWithErr{}, cmd: &MockCommand{}, resolvdConfigPath: createTempFileWithContents(""),
160170
dockerDaemonConfigPath: createTempFileWithContents("{}")}, wantErr: true},

0 commit comments

Comments
 (0)