From 7edc21b80b1df77131ce3dde5bfc450a7c20cd97 Mon Sep 17 00:00:00 2001 From: Jatin <84621253+h0x0er@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:54:34 +0530 Subject: [PATCH 01/25] restrict calls to dns_servers from agent-uid --- .gitignore | 5 ++++- firewall.go | 29 +++++++++++++++++++---------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 3aaaad7..9e06fcf 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,7 @@ coverage.txt .vscode/ vendor -private-src \ No newline at end of file +private-src + +dist +local \ No newline at end of file diff --git a/firewall.go b/firewall.go index e6e6bbf..19c1fad 100644 --- a/firewall.go +++ b/firewall.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "os" "github.com/coreos/go-iptables/iptables" "github.com/pkg/errors" @@ -77,6 +78,24 @@ func addBlockRules(firewall *Firewall, endpoints []ipAddressEndpoint, chain, net } } + // Agent uses HTTPs to resolve domain names + // Only apply UID filtering for OUTPUT chain + if chain == outputChain { + agentUID := fmt.Sprintf("%d", os.Getuid()) + for _, dnsServer := range dnsServers { + err = ipt.Append(filterTable, chain, direction, netInterface, + "-m", "owner", "--uid-owner", agentUID, + protocol, tcp, + destination, dnsServer, + destinationPort, "443", + target, accept) + + if err != nil { + return errors.Wrapf(err, "failed to add rule for DNS server %s", dnsServer) + } + } + } + for _, endpoint := range endpoints { err = ipt.Append(filterTable, chain, direction, netInterface, protocol, tcp, destination, endpoint.ipAddress, @@ -87,16 +106,6 @@ func addBlockRules(firewall *Firewall, endpoints []ipAddressEndpoint, chain, net } } - // Agent uses HTTPs to resolve domain names - for _, dnsServer := range dnsServers { - err = ipt.Append(filterTable, chain, direction, netInterface, protocol, tcp, - destination, dnsServer, target, accept) - - if err != nil { - return errors.Wrapf(err, "failed to add rule for DNS server %s", dnsServer) - } - } - // Allow AzureIPAddress err = ipt.Append(filterTable, chain, direction, netInterface, protocol, tcp, destination, AzureIPAddress, target, accept) From f3e4b8715e61002ccc5ece6e778f3606e1598dfe Mon Sep 17 00:00:00 2001 From: Ashish Kurmi Date: Mon, 16 Mar 2026 00:32:39 -0700 Subject: [PATCH 02/25] removing ai-codewise workflow as it's not required anymore --- .github/workflows/code-review.yml | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 .github/workflows/code-review.yml diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml deleted file mode 100644 index 2315798..0000000 --- a/.github/workflows/code-review.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Code Review -on: - pull_request: -permissions: - contents: read -jobs: - code-review: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - steps: - - name: Harden Runner - uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0 - with: - disable-sudo: true - egress-policy: block - allowed-endpoints: > - api.github.com:443 - int.api.stepsecurity.io:443 - - - name: Code Review - uses: step-security/ai-codewise@int - From 5d7504737fee418a619004507f76e2046722ce1d Mon Sep 17 00:00:00 2001 From: Rohan Prabhu Date: Mon, 16 Mar 2026 15:03:10 +0530 Subject: [PATCH 03/25] feat: update armour version (cherry picked from commit 378ef52c1dea39557e9067628ea5e51b42c367f4) --- go.mod | 65 ++++++++++++++---- go.sum | 204 +++++++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 224 insertions(+), 45 deletions(-) diff --git a/go.mod b/go.mod index e45f326..3e09d4e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/step-security/agent -go 1.24.1 +go 1.24.6 require ( github.com/coreos/go-iptables v0.6.0 @@ -8,39 +8,80 @@ require ( github.com/florianl/go-nflog/v2 v2.0.1 github.com/google/gopacket v1.1.19 github.com/jarcoal/httpmock v1.3.0 - github.com/miekg/dns v1.1.53 + github.com/miekg/dns v1.1.57 github.com/pkg/errors v0.9.1 - github.com/step-security/armour v1.1.0 + github.com/step-security/armour v1.1.3-0.20260313052812-7f78ae9efbc3 ) require ( github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/agnivade/levenshtein v1.2.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cilium/ebpf v0.17.3 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/docker/distribution v2.8.1+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/josharian/native v1.1.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lestrrat-go/blackmagic v1.0.4 // indirect + github.com/lestrrat-go/dsig v1.0.0 // indirect + github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect + github.com/lestrrat-go/jwx/v3 v3.0.12 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lestrrat-go/option/v2 v2.0.0 // indirect github.com/mdlayher/socket v0.4.1 // indirect github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/open-policy-agent/opa v1.12.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect - github.com/stretchr/testify v1.8.2 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect + github.com/segmentio/asm v1.2.1 // indirect + github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect + github.com/tchap/go-patricia/v2 v2.3.3 // indirect + github.com/valyala/fastjson v1.6.4 // indirect + github.com/vektah/gqlparser/v2 v2.5.31 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/yashtewari/glob-intersection v0.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/tools v0.38.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.2 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) require ( github.com/docker/docker v23.0.4+incompatible - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/mdlayher/netlink v1.7.2 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/sys v0.32.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sys v0.38.0 // indirect ) diff --git a/go.sum b/go.sum index ae1771d..ceac03d 100644 --- a/go.sum +++ b/go.sum @@ -2,14 +2,37 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bytecodealliance/wasmtime-go/v39 v39.0.1 h1:RibaT47yiyCRxMOj/l2cvL8cWiWBSqDXHyqsa9sGcCE= +github.com/bytecodealliance/wasmtime-go/v39 v39.0.1/go.mod h1:miR4NYIEBXeDNamZIzpskhJ0z/p8al+lwMWylQ/ZJb4= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/cilium/ebpf v0.17.3 h1:FnP4r16PWYSE4ux6zN+//jMcW4nMVRvuTLVTvCjyyjg= github.com/cilium/ebpf v0.17.3/go.mod h1:G5EDHij8yiLzaqn0WjyfJHvRa+3aDlReIaLVRMvOyJk= github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs= +github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w= +github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= +github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v23.0.4+incompatible h1:Kd3Bh9V/rO+XpTP/BLqM+gx8z7+Yb0AA2Ibj+nNo4ek= @@ -18,15 +41,36 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elastic/go-libaudit/v2 v2.3.2 h1:qWNcA3nkwNEGh1UBDbDTVF55KR6SM1W2Ji1LGDqFEpw= github.com/elastic/go-libaudit/v2 v2.3.2/go.mod h1:+ZE0czqmbqtnRkl0fNgpI+HvVVRo/ZMJdcXv/PaKcOo= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/florianl/go-nflog/v2 v2.0.1 h1:8csWIqFQ2vDaZJkxezY3dXDB7bEFg0VRFsYd2Bzj3II= github.com/florianl/go-nflog/v2 v2.0.1/go.mod h1:g+SOgM/SuePn9bvS/eo3Ild7J71nSb29OzbxR+7cln0= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -34,10 +78,14 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= @@ -58,6 +106,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -65,6 +115,22 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= +github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38= +github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI= +github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk= +github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg= +github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= +github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo= @@ -84,52 +150,109 @@ github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00/go.mod h1:GAFlyu4/XV68LkQKYzKhIo/WW7j3Zi0YRAz/BOoanUc= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= -github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw= -github.com/miekg/dns v1.1.53/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/open-policy-agent/opa v1.12.2 h1:Nh60UaIBP6NKCgy45jmMHZwNYALtNR9e/hj+Lna49mc= +github.com/open-policy-agent/opa v1.12.2/go.mod h1:RnDgm04GA1RjEXJvrsG9uNT/+FyBNmozcPvA2qz60M4= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8= -github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/step-security/armour v1.0.1 h1:+Lae8o/cbSV0HFD4wKhx4mHnQCTEJ8ndRN0gfmu1t3I= -github.com/step-security/armour v1.0.1/go.mod h1:I6pTEysb5fd3Cc79tvCMVp70RqhvMYbawfoq5Gz0cPI= -github.com/step-security/armour v1.0.4 h1:bTtvS4A9TTG83sSXW/+nno9cQOgqaueAedGdunE1eaY= -github.com/step-security/armour v1.0.4/go.mod h1:I6pTEysb5fd3Cc79tvCMVp70RqhvMYbawfoq5Gz0cPI= -github.com/step-security/armour v1.1.0 h1:oxJfxIOouf+KME4SzmZwukGsJSGlKmRR3ysExIeFAcY= -github.com/step-security/armour v1.1.0/go.mod h1:I6pTEysb5fd3Cc79tvCMVp70RqhvMYbawfoq5Gz0cPI= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0= +github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/step-security/armour v1.1.3-0.20260313052812-7f78ae9efbc3 h1:IeXOWf2MKEqgG6u1UdKUT6Dde9aceKVTMgEl22nzSPI= +github.com/step-security/armour v1.1.3-0.20260313052812-7f78ae9efbc3/go.mod h1:uopgPKW3HfuP05Wc0QeaN9buiy/XJH2B5HdNuM8sBLE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc= +github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= +github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k= +github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= +github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -145,13 +268,13 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -172,26 +295,39 @@ golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= @@ -201,3 +337,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= From 417ef2ecc43c6aba0958084493112c22f0e9d0c6 Mon Sep 17 00:00:00 2001 From: Rohan Prabhu Date: Mon, 16 Mar 2026 15:03:26 +0530 Subject: [PATCH 04/25] feat: integrate custom detection rules with Armour - Added support for custom detection rules in the Armour integration. - Introduced new functions to submit process, file, and network events to the detection manager. - Updated the DNS proxy to submit DNS events when custom detection rules are enabled. - Refactored the agent's DNS handling to ensure proper execution flow when Docker is uninstalled. (cherry picked from commit 8192dfe3b01a8db4fd40043917363adcbcd431f4) --- agent.go | 29 ++++++++++------ armour_manager.go | 31 +++++++++++++++++ common.go | 4 +++ dnsproxy.go | 20 +++++++++++ eventhandler.go | 74 +++++++++++++++++++++++++++++++++++++++++ global_feature_flags.go | 10 ++++-- 6 files changed, 155 insertions(+), 13 deletions(-) create mode 100644 armour_manager.go diff --git a/agent.go b/agent.go index a52b9d4..f252712 100644 --- a/agent.go +++ b/agent.go @@ -167,15 +167,18 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, WriteLog("\n") WriteLog("updated resolved") - // Change DNS for docker, causes process in containers to use agent's DNS proxy - if err := dnsConfig.SetDockerDNSServer(cmd, dockerDaemonConfigPath, tempDir); err != nil { - WriteLog(fmt.Sprintf("Error setting DNS server for docker %v", err)) - RevertChanges(iptables, nflog, cmd, resolvdConfigPath, dockerDaemonConfigPath, dnsConfig, sudo) - return err - } + // we uninstall docker using go routine, handle case where that routine finishes before we come here + if !config.DisableSudoAndContainers { + // Change DNS for docker, causes process in containers to use agent's DNS proxy + if err := dnsConfig.SetDockerDNSServer(cmd, dockerDaemonConfigPath, tempDir); err != nil { + WriteLog(fmt.Sprintf("Error setting DNS server for docker %v", err)) + RevertChanges(iptables, nflog, cmd, resolvdConfigPath, dockerDaemonConfigPath, dnsConfig, sudo) + return err + } - WriteLog("\n") - WriteLog("set docker config\n") + WriteLog("\n") + WriteLog("set docker config\n") + } if config.EgressPolicy == EgressPolicyAudit { netMonitor := NetworkMonitor{ @@ -242,13 +245,17 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, conf.Files = append(conf.Files, getFilesOfInterest()...) - mArmour := armour.NewArmour(ctx, conf) - err := mArmour.Attach() + err := InitArmour(ctx, conf) if err != nil { WriteLog("Armour attachment failed") } else { - defer mArmour.Detach() + if GlobalArmour != nil { + defer GlobalArmour.Detach() + } WriteLog("Armour attached") + if IsCustomDetectionRulesEnabled() { + WriteLog("[armour] Custom detection rules enabled") + } } } diff --git a/armour_manager.go b/armour_manager.go new file mode 100644 index 0000000..2dd7529 --- /dev/null +++ b/armour_manager.go @@ -0,0 +1,31 @@ +package main + +import ( + "context" + "fmt" + + "github.com/step-security/armour/armour" +) + +// NOTE: before usage, make sure to nil check +var GlobalArmour *armour.Armour = nil + +func InitArmour(ctx context.Context, conf *armour.Config) error { + + GlobalArmour = armour.NewArmour(ctx, conf) + err := GlobalArmour.Init() + if err != nil { + GlobalArmour = nil + return err + } + + runnerWorkerPID, err := getRunnerWorkerPID() + if err != nil { + WriteLog(fmt.Sprintf("[armour] Error getting Runner.Worker PID: %v", err)) + return nil + } + GlobalArmour.SetRunnerWorkerPID(runnerWorkerPID) + WriteLog(fmt.Sprintf("[armour] Runner.Worker PID: %d", runnerWorkerPID)) + + return nil +} diff --git a/common.go b/common.go index 9b44739..55a35e1 100644 --- a/common.go +++ b/common.go @@ -91,3 +91,7 @@ func getProcMemFiles(pid uint64) []string { return out } + +func getRunnerWorkerPID() (uint64, error) { + return pidOf("Runner.Worker") +} diff --git a/dnsproxy.go b/dnsproxy.go index 87d5a38..47e0d70 100644 --- a/dnsproxy.go +++ b/dnsproxy.go @@ -11,6 +11,7 @@ import ( "github.com/miekg/dns" "github.com/pkg/errors" + "github.com/step-security/armour/armour" ) type DNSProxy struct { @@ -239,6 +240,8 @@ func (proxy *DNSProxy) getIPByDomain(domain string) (string, error) { go proxy.ApiClient.sendDNSRecord(proxy.CorrelationId, proxy.Repo, domain, answer.Data) + go proxy.submitDNSEvent(answer.Data) + return answer.Data, nil } @@ -296,6 +299,23 @@ func (proxy *DNSProxy) processTypeA(q *dns.Question, requestMsg *dns.Msg) (*dns. return &rr, nil } +// submitDNSEvent submits a DNS event to the detection manager. +func (proxy *DNSProxy) submitDNSEvent(dest string) { + if !IsCustomDetectionRulesEnabled() { + return + } + if GlobalArmour == nil { + return + } + dm := GlobalArmour.DetectionManager() + if dm == nil { + return + } + dm.SubmitNetwork(&armour.NetworkDetectionEvent{ + Dest: dest, + }) +} + func startDNSServer(dnsProxy *DNSProxy, server DNSServer, errc chan error) { dns.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) { switch r.Opcode { diff --git a/eventhandler.go b/eventhandler.go index 85bb864..cbac4f1 100644 --- a/eventhandler.go +++ b/eventhandler.go @@ -15,6 +15,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/client" + "github.com/step-security/armour/armour" ) type EventHandler struct { @@ -89,6 +90,8 @@ func (eventHandler *EventHandler) handleFileEvent(event *Event) { } } + eventHandler.submitFileEvent(event) + eventHandler.fileMutex.Unlock() } @@ -123,6 +126,8 @@ func (eventHandler *EventHandler) handleProcessEvent(event *Event) { } else { eventHandler.procMutex.Unlock() } + + eventHandler.submitProcessEvent(event) } /* @@ -199,6 +204,10 @@ func (eventHandler *EventHandler) handleNetworkEvent(event *Event) { } eventHandler.netMutex.Unlock() + + eventHandler.submitDNSEvent(reverseLookUp) + + eventHandler.submitNetworkEvent(event) } func (eventHandler *EventHandler) HandleEvent(event *Event) { @@ -440,3 +449,68 @@ func isPrivateIPAddress(ipAddress string) bool { func isIPv6(ip string) bool { return strings.Contains(ip, ":") } + +func (eventHandler *EventHandler) submitProcessEvent(event *Event) { + if !IsCustomDetectionRulesEnabled() { + return + } + if GlobalArmour == nil { + return + } + dm := GlobalArmour.DetectionManager() + if dm == nil { + return + } + dm.SubmitProcess(&armour.ProcessDetectionEvent{ + Pid: event.Pid, + PPid: event.PPid, + Exe: event.Exe, + Arguments: event.ProcessArguments, + Cwd: event.Path, + Timestamp: event.Timestamp, + }) +} + +// submitFileEvent submits a file event to the detection manager. +func (eventHandler *EventHandler) submitFileEvent(event *Event) { + if !IsCustomDetectionRulesEnabled() { + return + } + if GlobalArmour == nil { + return + } + dm := GlobalArmour.DetectionManager() + if dm == nil { + return + } + dm.SubmitFile(&armour.FileDetectionEvent{ + Syscall: event.Syscall, + FileName: filepath.Base(event.FileName), + Path: event.FileName, + Exe: event.Exe, + Pid: event.Pid, + PPid: event.PPid, + Timestamp: event.Timestamp, + }) +} + +// submitNetworkEvent submits a network event to the detection manager. +func (eventHandler *EventHandler) submitNetworkEvent(event *Event) { + if GlobalArmour == nil { + return + } + dm := GlobalArmour.DetectionManager() + if dm == nil { + return + } + + dm.SubmitNetwork(&armour.NetworkDetectionEvent{ + Pid: event.Pid, + PPid: event.PPid, + Exe: event.Exe, + Dest: event.IPAddress, + DestIP: event.IPAddress, + DestPort: event.Port, + Timestamp: event.Timestamp, + }) +} diff --git a/global_feature_flags.go b/global_feature_flags.go index 21e0b69..f760a73 100644 --- a/global_feature_flags.go +++ b/global_feature_flags.go @@ -11,8 +11,9 @@ const ( ) type GlobalFeatureFlags struct { - AgentType string `json:"agent_type"` - EnableArmour bool `json:"enable_armour"` + AgentType string `json:"agent_type"` + EnableArmour bool `json:"enable_armour"` + EnableCustomDetectionRules bool `json:"enable_custom_detection_rules"` } // GlobalFeatureFlagManager manages fetching and caching of global feature flags. @@ -79,3 +80,8 @@ func IsArmourEnabled() bool { flags := GetGlobalFeatureFlags() return flags.EnableArmour } + +func IsCustomDetectionRulesEnabled() bool { + flags := GetGlobalFeatureFlags() + return flags.EnableCustomDetectionRules +} From 6e69a37a8675c551f9d7a870736cc71871106301 Mon Sep 17 00:00:00 2001 From: Rohan Prabhu Date: Mon, 16 Mar 2026 15:52:05 +0530 Subject: [PATCH 05/25] feat: add TelemetryURL support to ApiClient and configuration - Introduced TelemetryURL in the ApiClient struct to allow separate telemetry endpoint usage. - Updated the agent to utilize TelemetryURL for sending DNS records and network connections. - Enhanced configuration handling to initialize TelemetryURL from the config file, defaulting to APIURL if not specified. (cherry picked from commit 93e00d3a5d07f5f331b053753b851ea1a89180ce) --- agent.go | 2 +- apiclient.go | 5 +++-- config.go | 6 ++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/agent.go b/agent.go index f252712..e5e6eef 100644 --- a/agent.go +++ b/agent.go @@ -67,7 +67,7 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, return err } - apiclient := &ApiClient{Client: &http.Client{Timeout: 3 * time.Second}, APIURL: config.APIURL, DisableTelemetry: config.DisableTelemetry, EgressPolicy: config.EgressPolicy, OneTimeKey: config.OneTimeKey} + apiclient := &ApiClient{Client: &http.Client{Timeout: 3 * time.Second}, APIURL: config.APIURL, TelemetryURL: config.TelemetryURL, DisableTelemetry: config.DisableTelemetry, EgressPolicy: config.EgressPolicy, OneTimeKey: config.OneTimeKey} config.OneTimeKey = "" // TODO: pass in an iowriter/ use log library diff --git a/apiclient.go b/apiclient.go index 6bc62e3..00342bf 100644 --- a/apiclient.go +++ b/apiclient.go @@ -41,6 +41,7 @@ type NetworkConnection struct { type ApiClient struct { Client *http.Client APIURL string + TelemetryURL string DisableTelemetry bool EgressPolicy string OneTimeKey string @@ -57,7 +58,7 @@ func (apiclient *ApiClient) sendDNSRecord(correlationId, repo, domainName, ipAdd dnsRecord.ResolvedIPAddress = ipAddress dnsRecord.TimeStamp = time.Now().UTC() - url := fmt.Sprintf("%s/github/%s/actions/jobs/%s/dns", apiclient.APIURL, repo, correlationId) + url := fmt.Sprintf("%s/github/%s/actions/jobs/%s/dns", apiclient.TelemetryURL, repo, correlationId) return apiclient.sendApiRequest("POST", url, dnsRecord) } @@ -76,7 +77,7 @@ func (apiclient *ApiClient) sendNetConnection(correlationId, repo, ipAddress, po networkConnection.TimeStamp = timestamp networkConnection.Tool = tool - url := fmt.Sprintf("%s/github/%s/actions/jobs/%s/networkconnection", apiclient.APIURL, repo, correlationId) + url := fmt.Sprintf("%s/github/%s/actions/jobs/%s/networkconnection", apiclient.TelemetryURL, repo, correlationId) return apiclient.sendApiRequest("POST", url, networkConnection) } diff --git a/config.go b/config.go index d63f1b3..61a7793 100644 --- a/config.go +++ b/config.go @@ -16,6 +16,7 @@ type config struct { RunId string WorkingDirectory string APIURL string + TelemetryURL string OneTimeKey string Endpoints map[string][]Endpoint EgressPolicy string @@ -37,6 +38,7 @@ type configFile struct { RunId string `json:"run_id"` WorkingDirectory string `json:"working_directory"` APIURL string `json:"api_url"` + TelemetryURL string `json:"telemetry_url"` OneTimeKey string `json:"one_time_key"` AllowedEndpoints string `json:"allowed_endpoints"` EgressPolicy string `json:"egress_policy"` @@ -65,6 +67,10 @@ func (c *config) init(configFilePath string) error { c.RunId = configFile.RunId c.WorkingDirectory = configFile.WorkingDirectory c.APIURL = configFile.APIURL + c.TelemetryURL = configFile.TelemetryURL + if c.TelemetryURL == "" { + c.TelemetryURL = c.APIURL + } c.Endpoints = parseEndpoints(configFile.AllowedEndpoints) c.EgressPolicy = configFile.EgressPolicy c.DisableTelemetry = configFile.DisableTelemetry From 5c92d7624dcd939e70415fcc711e51ca3816be29 Mon Sep 17 00:00:00 2001 From: Rohan Prabhu Date: Mon, 16 Mar 2026 15:53:36 +0530 Subject: [PATCH 06/25] refactor: remove redundant DNS event submission in handleNetworkEvent - Eliminated the call to submitDNSEvent in the handleNetworkEvent function, streamlining the event handling process. (cherry picked from commit a1eac7b30b282c7a261d703890ebfa8a573ba9b6) --- eventhandler.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/eventhandler.go b/eventhandler.go index cbac4f1..0f6c99d 100644 --- a/eventhandler.go +++ b/eventhandler.go @@ -205,8 +205,6 @@ func (eventHandler *EventHandler) handleNetworkEvent(event *Event) { eventHandler.netMutex.Unlock() - eventHandler.submitDNSEvent(reverseLookUp) - eventHandler.submitNetworkEvent(event) } From bbc2a1b8bbbe3f46f174dec824bccf31e13fc25a Mon Sep 17 00:00:00 2001 From: Rohan Prabhu Date: Mon, 16 Mar 2026 16:19:26 +0530 Subject: [PATCH 07/25] feat: whitelist telemetry endpoint for StepSecurity (cherry picked from commit ee6926727b5802e709b788b0b3b44e4367ad2237) --- agent.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/agent.go b/agent.go index e5e6eef..aed9652 100644 --- a/agent.go +++ b/agent.go @@ -377,10 +377,13 @@ func addImplicitEndpoints(endpoints map[string][]Endpoint, disableTelemetry bool } } - stepsecurity := Endpoint{domainName: "agent.api.stepsecurity.io", port: 443} // Should be implicit based on user feedback + stepsecurity := Endpoint{domainName: "agent.api.stepsecurity.io", port: 443} // Should be implicit based on user feedback + stepsecurityTelemetry := Endpoint{domainName: "prod.app-api.stepsecurity.io", port: 443} // Telemetry endpoint for sending DNS and net connections to StepSecurity + if !disableTelemetry { // allowing only if disable_telemetry is set to false normalEndpoints[stepsecurity.domainName] = append(normalEndpoints[stepsecurity.domainName], stepsecurity) + normalEndpoints[stepsecurityTelemetry.domainName] = append(normalEndpoints[stepsecurityTelemetry.domainName], stepsecurityTelemetry) } return normalEndpoints, wildcardEndpoints From 32bd497784f636b3f6949c4fc8deef2e779978ba Mon Sep 17 00:00:00 2001 From: Rohan Prabhu Date: Tue, 17 Mar 2026 16:56:35 +0530 Subject: [PATCH 08/25] feat: enable custom detection rules in agent configuration - Added a new configuration option to enable custom detection rules in the agent's run function. (cherry picked from commit 75a349ee97f1ef2d694d5a8a9227856245accdb8) --- agent.go | 1 + 1 file changed, 1 insertion(+) diff --git a/agent.go b/agent.go index aed9652..0e52af1 100644 --- a/agent.go +++ b/agent.go @@ -239,6 +239,7 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, OneTimeKey: config.OneTimeKey, DisableTelemetry: config.DisableTelemetry, }, + EnableCustomDetectionRules: IsCustomDetectionRulesEnabled(), } conf.Files = append(conf.Files, getProcFilesOfInterest()...) From f8eed067749f4468c704e20cd6d0e145d6c1e8cd Mon Sep 17 00:00:00 2001 From: Rohan Prabhu Date: Wed, 18 Mar 2026 17:59:21 +0530 Subject: [PATCH 09/25] fix: update OneTimeKey assignment and upgrade Armour dependency - Changed the assignment of OneTimeKey in the agent's run function to use apiclient.OneTimeKey. - Updated the Armour dependency version from v1.1.3 to v1.2.0 in go.mod and go.sum. (cherry picked from commit d4bfe1bf2c46301268c3582a7694e8a3fb341959) --- agent.go | 2 +- go.mod | 2 +- go.sum | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/agent.go b/agent.go index 0e52af1..e002944 100644 --- a/agent.go +++ b/agent.go @@ -236,7 +236,7 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, APIURL: config.APIURL, Repo: config.Repo, CorrelationID: config.CorrelationId, - OneTimeKey: config.OneTimeKey, + OneTimeKey: apiclient.OneTimeKey, DisableTelemetry: config.DisableTelemetry, }, EnableCustomDetectionRules: IsCustomDetectionRulesEnabled(), diff --git a/go.mod b/go.mod index 3e09d4e..9c00d00 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/jarcoal/httpmock v1.3.0 github.com/miekg/dns v1.1.57 github.com/pkg/errors v0.9.1 - github.com/step-security/armour v1.1.3-0.20260313052812-7f78ae9efbc3 + github.com/step-security/armour v1.2.0 ) require ( diff --git a/go.sum b/go.sum index ceac03d..31c6cac 100644 --- a/go.sum +++ b/go.sum @@ -189,6 +189,8 @@ github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yf github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/step-security/armour v1.1.3-0.20260313052812-7f78ae9efbc3 h1:IeXOWf2MKEqgG6u1UdKUT6Dde9aceKVTMgEl22nzSPI= github.com/step-security/armour v1.1.3-0.20260313052812-7f78ae9efbc3/go.mod h1:uopgPKW3HfuP05Wc0QeaN9buiy/XJH2B5HdNuM8sBLE= +github.com/step-security/armour v1.2.0 h1:DzGsX0HOWiMJpD8wo5k5bJKohWbjk3WYm6x13UNzsTE= +github.com/step-security/armour v1.2.0/go.mod h1:uopgPKW3HfuP05Wc0QeaN9buiy/XJH2B5HdNuM8sBLE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= From 44012f7f2e9eea77541566efc11ccddacc5a8e40 Mon Sep 17 00:00:00 2001 From: Rohan Prabhu Date: Wed, 18 Mar 2026 18:03:36 +0530 Subject: [PATCH 10/25] chore: update OpenTelemetry dependencies to v1.40.0 and golang.org/x/sys to v0.40.0 - Upgraded OpenTelemetry packages in go.mod and go.sum to version 1.40.0. - Updated golang.org/x/sys dependency to version 0.40.0. (cherry picked from commit 72f6f12c0bb5e717f9bf3ff605a9fd5d15899db1) --- go.mod | 10 +++++----- go.sum | 26 ++++++++++++-------------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index 9c00d00..d0c9abe 100644 --- a/go.mod +++ b/go.mod @@ -61,10 +61,10 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/sdk v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/sdk v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/crypto v0.45.0 // indirect @@ -83,5 +83,5 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/mdlayher/netlink v1.7.2 // indirect golang.org/x/net v0.47.0 // indirect - golang.org/x/sys v0.38.0 // indirect + golang.org/x/sys v0.40.0 // indirect ) diff --git a/go.sum b/go.sum index 31c6cac..7974291 100644 --- a/go.sum +++ b/go.sum @@ -187,8 +187,6 @@ github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0= github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/step-security/armour v1.1.3-0.20260313052812-7f78ae9efbc3 h1:IeXOWf2MKEqgG6u1UdKUT6Dde9aceKVTMgEl22nzSPI= -github.com/step-security/armour v1.1.3-0.20260313052812-7f78ae9efbc3/go.mod h1:uopgPKW3HfuP05Wc0QeaN9buiy/XJH2B5HdNuM8sBLE= github.com/step-security/armour v1.2.0 h1:DzGsX0HOWiMJpD8wo5k5bJKohWbjk3WYm6x13UNzsTE= github.com/step-security/armour v1.2.0/go.mod h1:uopgPKW3HfuP05Wc0QeaN9buiy/XJH2B5HdNuM8sBLE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -216,22 +214,22 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -298,8 +296,8 @@ golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From 6b744561de4eae98871ccc0ff446f68e2b61394a Mon Sep 17 00:00:00 2001 From: Rohan Prabhu Date: Mon, 23 Mar 2026 12:41:38 +0530 Subject: [PATCH 11/25] refactor: reorder file event submission in handleFileEvent - Moved the call to submitFileEvent to occur after unlocking the fileMutex, improving the event handling flow. (cherry picked from commit 025b442b353466d5c040e31d2571f89ec3b6553a) --- eventhandler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eventhandler.go b/eventhandler.go index 0f6c99d..4548ed0 100644 --- a/eventhandler.go +++ b/eventhandler.go @@ -90,9 +90,9 @@ func (eventHandler *EventHandler) handleFileEvent(event *Event) { } } - eventHandler.submitFileEvent(event) - eventHandler.fileMutex.Unlock() + + eventHandler.submitFileEvent(event) } func isSourceCodeFile(fileName string) bool { From 12ba4b8a1751c92f94941207dd67f29b67b67f9f Mon Sep 17 00:00:00 2001 From: Rohan Prabhu Date: Fri, 3 Apr 2026 11:51:37 +0530 Subject: [PATCH 12/25] chore: update Armour dependency to v1.2.1 - Upgraded the Armour dependency version in go.mod and go.sum from v1.2.0 to v1.2.1. (cherry picked from commit 6d860628b0380470f3e8d670cd8b6e9e35f63309) --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index d0c9abe..feee67c 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/jarcoal/httpmock v1.3.0 github.com/miekg/dns v1.1.57 github.com/pkg/errors v0.9.1 - github.com/step-security/armour v1.2.0 + github.com/step-security/armour v1.2.1 ) require ( diff --git a/go.sum b/go.sum index 7974291..86e9aec 100644 --- a/go.sum +++ b/go.sum @@ -189,6 +189,8 @@ github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yf github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/step-security/armour v1.2.0 h1:DzGsX0HOWiMJpD8wo5k5bJKohWbjk3WYm6x13UNzsTE= github.com/step-security/armour v1.2.0/go.mod h1:uopgPKW3HfuP05Wc0QeaN9buiy/XJH2B5HdNuM8sBLE= +github.com/step-security/armour v1.2.1 h1:WFhrnUanh60c+jUXBsEX01jS5pi51Cs28wq//tLiuqU= +github.com/step-security/armour v1.2.1/go.mod h1:uopgPKW3HfuP05Wc0QeaN9buiy/XJH2B5HdNuM8sBLE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= From 9543dcec148279305d753965f8c12beffad81357 Mon Sep 17 00:00:00 2001 From: Jatin <84621253+h0x0er@users.noreply.github.com> Date: Sun, 19 Apr 2026 21:07:53 +0530 Subject: [PATCH 13/25] Merge pull request #478 from step-security/exp/blocklist Add blocklist changes --- agent.go | 72 +++++++++++++---- agent_test.go | 35 +++++++++ apiclient.go | 56 +++++++++++-- dnsproxy.go | 17 +++- dnsproxy_test.go | 38 +++++++++ eventhandler.go | 62 ++++++++------- firewall.go | 86 +++++++++++++++++++- firewall_blocklist_test.go | 107 +++++++++++++++++++++++++ global_blocklist.go | 156 +++++++++++++++++++++++++++++++++++++ netmon.go | 38 ++++++--- 10 files changed, 600 insertions(+), 67 deletions(-) create mode 100644 firewall_blocklist_test.go create mode 100644 global_blocklist.go diff --git a/agent.go b/agent.go index e002944..5c26893 100644 --- a/agent.go +++ b/agent.go @@ -80,6 +80,16 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, InitGlobalFeatureFlags(config.APIURL, apiclient) WriteLog("initialized global feature flags") WriteLog("\n") + + globalBlocklistResponse, err := apiclient.getGlobalBlocklist() + globalBlocklist := NewGlobalBlocklist(globalBlocklistResponse) + if err != nil { + WriteLog(fmt.Sprintf("Error initializing global blocklist: %v", err)) + } else { + WriteLog(fmt.Sprintf("initialized global blocklist: %+v", globalBlocklist)) + WriteLog("\n") + } + if IsArmourEnabled() { lf := lockfile.New(agentLockFile) if err := lf.TryLock(); err != nil { @@ -104,7 +114,7 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, Cache := InitCache(config.EgressPolicy) - allowedEndpoints, wildcardEndpoints := addImplicitEndpoints(config.Endpoints, config.DisableTelemetry) + allowedEndpoints, wildcardEndpoints := addImplicitEndpoints(config.Endpoints, config.DisableTelemetry, globalBlocklist) // Start DNS servers and get confirmation dnsProxy := DNSProxy{ @@ -113,6 +123,7 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, Repo: config.Repo, ApiClient: apiclient, EgressPolicy: config.EgressPolicy, + GlobalBlocklist: globalBlocklist, AllowedEndpoints: allowedEndpoints, WildCardEndpoints: wildcardEndpoints, ReverseIPLookup: make(map[string]string), @@ -182,10 +193,11 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, if config.EgressPolicy == EgressPolicyAudit { netMonitor := NetworkMonitor{ - CorrelationId: config.CorrelationId, - Repo: config.Repo, - ApiClient: apiclient, - Status: "Allowed", + CorrelationId: config.CorrelationId, + Repo: config.Repo, + ApiClient: apiclient, + GlobalBlocklist: globalBlocklist, + Status: "Allowed", } // Start network monitor @@ -208,10 +220,11 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, WriteLog("\n") netMonitor := NetworkMonitor{ - CorrelationId: config.CorrelationId, - Repo: config.Repo, - ApiClient: apiclient, - Status: "Dropped", + CorrelationId: config.CorrelationId, + Repo: config.Repo, + ApiClient: apiclient, + GlobalBlocklist: globalBlocklist, + Status: "Dropped", } // Start network monitor @@ -223,7 +236,15 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, return err } - go refreshDNSEntries(ctx, iptables, allowedEndpoints, &dnsProxy) + go refreshDNSEntries(ctx, iptables, globalBlocklist, allowedEndpoints, &dnsProxy) + } + + if config.EgressPolicy == EgressPolicyAudit || config.EgressPolicy == EgressPolicyBlock { + if err := AddGlobalBlockRules(iptables, globalBlocklist); err != nil { + WriteLog(fmt.Sprintf("Error adding global blocklist firewall rules %v", err)) + RevertChanges(iptables, nflog, cmd, resolvdConfigPath, dockerDaemonConfigPath, dnsConfig, sudo) + return err + } } if IsArmourEnabled() { @@ -296,7 +317,7 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, } } -func refreshDNSEntries(ctx context.Context, iptables *Firewall, allowedEndpoints map[string][]Endpoint, dnsProxy *DNSProxy) { +func refreshDNSEntries(ctx context.Context, iptables *Firewall, blocklist *GlobalBlocklist, allowedEndpoints map[string][]Endpoint, dnsProxy *DNSProxy) { ticker := time.NewTicker(30 * time.Second) go func() { for { @@ -323,7 +344,7 @@ func refreshDNSEntries(ctx context.Context, iptables *Firewall, allowedEndpoints for _, endpoint := range endpoints { // add endpoint to firewall - err = InsertAllowRule(iptables, answer.Data, fmt.Sprintf("%d", endpoint.port)) + err = InsertAllowRule(iptables, blocklist, answer.Data, fmt.Sprintf("%d", endpoint.port)) if err != nil { break } @@ -348,7 +369,7 @@ func refreshDNSEntries(ctx context.Context, iptables *Firewall, allowedEndpoints } -func addImplicitEndpoints(endpoints map[string][]Endpoint, disableTelemetry bool) (map[string][]Endpoint, map[string][]Endpoint) { +func addImplicitEndpoints(endpoints map[string][]Endpoint, disableTelemetry bool, blocklist *GlobalBlocklist) (map[string][]Endpoint, map[string][]Endpoint) { normalEndpoints := make(map[string][]Endpoint) wildcardEndpoints := make(map[string][]Endpoint) @@ -387,7 +408,30 @@ func addImplicitEndpoints(endpoints map[string][]Endpoint, disableTelemetry bool normalEndpoints[stepsecurityTelemetry.domainName] = append(normalEndpoints[stepsecurityTelemetry.domainName], stepsecurityTelemetry) } - return normalEndpoints, wildcardEndpoints + if blocklist == nil { + return normalEndpoints, wildcardEndpoints + } + + filteredAllowedEndpoints := make(map[string][]Endpoint) + filteredWildcardEndpoints := make(map[string][]Endpoint) + + for domainName, endpoints := range normalEndpoints { + if blocked, reason := blocklist.IsDomainBlocked(domainName); blocked { + WriteLog(fmt.Sprintf("removing globally blocklisted allowed endpoint %s reason: %s", domainName, reason)) + continue + } + filteredAllowedEndpoints[domainName] = endpoints + } + + for domainName, endpoints := range wildcardEndpoints { + if blocked, reason := blocklist.IsWildcardDomainBlocked(domainName); blocked { + WriteLog(fmt.Sprintf("removing globally blocklisted wildcard endpoint %s reason: %s", domainName, reason)) + continue + } + filteredWildcardEndpoints[domainName] = endpoints + } + + return filteredAllowedEndpoints, filteredWildcardEndpoints } func RevertChanges(iptables *Firewall, nflog AgentNflogger, diff --git a/agent_test.go b/agent_test.go index 0daf7ff..1ecf212 100644 --- a/agent_test.go +++ b/agent_test.go @@ -162,6 +162,9 @@ func TestRun(t *testing.T) { httpmock.RegisterResponder("GET", "https://apiurl/v1/global-feature-flags?agent_type=agent-oss&version=", httpmock.NewStringResponder(200, `{"agent_type":"agent-oss","enable_armour":false}`)) + httpmock.RegisterResponder("GET", "https://apiurl/v1/global-blocklist", + httpmock.NewStringResponder(200, `{"ip_addresses":[],"domains":[],"wildcard_domains":[]}`)) + tests := []struct { name string args args @@ -242,6 +245,38 @@ func TestRun(t *testing.T) { } } +func Test_addImplicitEndpoints_RemovesGlobalBlocklistedEndpoints(t *testing.T) { + globalBlocklist := NewGlobalBlocklist(&GlobalBlocklistResponse{ + Domains: []CompromisedEndpoint{{Endpoint: "allowed.com", Reason: "blocked"}}, + WildcardDomains: []CompromisedEndpoint{{Endpoint: "*.githubusercontent.com", Reason: "blocked"}}, + }) + + endpoints := map[string][]Endpoint{ + "allowed.com.": {{domainName: "allowed.com.", port: 443}}, + "safe.com.": {{domainName: "safe.com.", port: 443}}, + "*.githubusercontent.com.": {{domainName: "*.githubusercontent.com.", port: 443}}, + "*.example.com.": {{domainName: "*.example.com.", port: 443}}, + } + + filteredAllowedEndpoints, filteredWildcardEndpoints := addImplicitEndpoints(endpoints, true, globalBlocklist) + + if _, found := filteredAllowedEndpoints["allowed.com."]; found { + t.Fatalf("expected allowed.com to be removed") + } + + if _, found := filteredAllowedEndpoints["safe.com."]; !found { + t.Fatalf("expected safe.com to remain") + } + + if _, found := filteredWildcardEndpoints["*.githubusercontent.com."]; found { + t.Fatalf("expected *.githubusercontent.com to be removed") + } + + if _, found := filteredWildcardEndpoints["*.example.com."]; !found { + t.Fatalf("expected *.example.com to remain") + } +} + func Test_writeDone(t *testing.T) { tests := []struct { name string diff --git a/apiclient.go b/apiclient.go index 00342bf..aae6bef 100644 --- a/apiclient.go +++ b/apiclient.go @@ -15,6 +15,8 @@ type DNSRecord struct { DomainName string `json:"domainName"` ResolvedIPAddress string `json:"ipAddress"` TimeStamp time.Time `json:"timestamp"` + MatchedPolicy string `json:"matched_policy,omitempty"` + Reason string `json:"reason,omitempty"` } type Tool struct { @@ -30,12 +32,14 @@ type FileEvent struct { } type NetworkConnection struct { - IPAddress string `json:"ipAddress,omitempty"` - Port string `json:"port,omitempty"` - DomainName string `json:"domainName,omitempty"` - TimeStamp time.Time `json:"timestamp"` - Tool Tool `json:"tool"` - Status string `json:"status,omitempty"` + IPAddress string `json:"ipAddress,omitempty"` + Port string `json:"port,omitempty"` + DomainName string `json:"domainName,omitempty"` + TimeStamp time.Time `json:"timestamp"` + Tool Tool `json:"tool"` + Status string `json:"status,omitempty"` + MatchedPolicy string `json:"matched_policy,omitempty"` + Reason string `json:"reason,omitempty"` } type ApiClient struct { @@ -49,7 +53,7 @@ type ApiClient struct { const agentApiBaseUrl = "https://apiurl/v1" -func (apiclient *ApiClient) sendDNSRecord(correlationId, repo, domainName, ipAddress string) error { +func (apiclient *ApiClient) sendDNSRecord(correlationId, repo, domainName, ipAddress, matchedPolicy, reason string) error { if !apiclient.DisableTelemetry || apiclient.EgressPolicy == EgressPolicyAudit { dnsRecord := &DNSRecord{} @@ -57,6 +61,8 @@ func (apiclient *ApiClient) sendDNSRecord(correlationId, repo, domainName, ipAdd dnsRecord.DomainName = domainName dnsRecord.ResolvedIPAddress = ipAddress dnsRecord.TimeStamp = time.Now().UTC() + dnsRecord.MatchedPolicy = matchedPolicy + dnsRecord.Reason = reason url := fmt.Sprintf("%s/github/%s/actions/jobs/%s/dns", apiclient.TelemetryURL, repo, correlationId) @@ -65,7 +71,7 @@ func (apiclient *ApiClient) sendDNSRecord(correlationId, repo, domainName, ipAdd return nil } -func (apiclient *ApiClient) sendNetConnection(correlationId, repo, ipAddress, port, domainName, status string, timestamp time.Time, tool Tool) error { +func (apiclient *ApiClient) sendNetConnection(correlationId, repo, ipAddress, port, domainName, status, matchedPolicy, reason string, timestamp time.Time, tool Tool) error { if !apiclient.DisableTelemetry || apiclient.EgressPolicy == EgressPolicyAudit { networkConnection := &NetworkConnection{} @@ -76,6 +82,8 @@ func (apiclient *ApiClient) sendNetConnection(correlationId, repo, ipAddress, po networkConnection.Status = status networkConnection.TimeStamp = timestamp networkConnection.Tool = tool + networkConnection.MatchedPolicy = matchedPolicy + networkConnection.Reason = reason url := fmt.Sprintf("%s/github/%s/actions/jobs/%s/networkconnection", apiclient.TelemetryURL, repo, correlationId) @@ -153,6 +161,38 @@ func (apiclient *ApiClient) getGlobalFeatureFlags() GlobalFeatureFlags { return globalFeatureFlags } +func (apiclient *ApiClient) getGlobalBlocklist() (*GlobalBlocklistResponse, error) { + url := fmt.Sprintf("%s/global-blocklist", apiclient.APIURL) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("[getGlobalBlocklist] error occurred while getting global blocklist: %s", err) + } + + resp, err := apiclient.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("[getGlobalBlocklist] error while sending request: %s", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("[getGlobalBlocklist] response status not okay: status %d", resp.StatusCode) + } + + reqBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("[getGlobalBlocklist] unable to read reqBody: %s", err) + } + + out := new(GlobalBlocklistResponse) + err = json.Unmarshal(reqBody, out) + if err != nil { + return nil, fmt.Errorf("[getGlobalBlocklist] unable to unmarshal reqBody: %s", err) + } + + return out, nil +} + func (apiclient *ApiClient) sendApiRequest(method, url string, body interface{}) error { jsonData, _ := json.Marshal(body) diff --git a/dnsproxy.go b/dnsproxy.go index 47e0d70..454dfeb 100644 --- a/dnsproxy.go +++ b/dnsproxy.go @@ -20,6 +20,7 @@ type DNSProxy struct { Repo string ApiClient *ApiClient EgressPolicy string + GlobalBlocklist *GlobalBlocklist AllowedEndpoints map[string][]Endpoint WildCardEndpoints map[string][]Endpoint ReverseIPLookup map[string]string @@ -191,6 +192,16 @@ func (proxy *DNSProxy) getIPByDomain(domain string) (string, error) { matchesAnyWildcard := false wildcardPort := "" + if proxy.GlobalBlocklist != nil { + if blocked, reason := proxy.GlobalBlocklist.IsDomainBlocked(domain); blocked { + go WriteLog(fmt.Sprintf("domain resolution blocked: %s, matched_policy: %s, reason: %s", domain, GlobalBlocklistMatchedPolicy, reason)) + proxy.Cache.Set(domain, &Answer{Name: domain, TTL: math.MaxInt32, Data: StepSecuritySinkHoleIPAddress}, false) + go proxy.ApiClient.sendDNSRecord(proxy.CorrelationId, proxy.Repo, domain, StepSecuritySinkHoleIPAddress, GlobalBlocklistMatchedPolicy, reason) + + return StepSecuritySinkHoleIPAddress, nil + } + } + if proxy.EgressPolicy == EgressPolicyBlock { if strings.HasSuffix(domain, ".internal.") { go WriteLog(fmt.Sprintf("unable to resolve internal domains: %s", domain)) @@ -215,7 +226,7 @@ func (proxy *DNSProxy) getIPByDomain(domain string) (string, error) { // the call will be blocked by the firewall proxy.Cache.Set(domain, &Answer{Name: domain, TTL: math.MaxInt32, Data: StepSecuritySinkHoleIPAddress}, false) - go proxy.ApiClient.sendDNSRecord(proxy.CorrelationId, proxy.Repo, domain, StepSecuritySinkHoleIPAddress) + go proxy.ApiClient.sendDNSRecord(proxy.CorrelationId, proxy.Repo, domain, StepSecuritySinkHoleIPAddress, "", "") return StepSecuritySinkHoleIPAddress, nil } @@ -229,7 +240,7 @@ func (proxy *DNSProxy) getIPByDomain(domain string) (string, error) { } if matchesAnyWildcard { - if err := InsertAllowRule(proxy.Iptables, answer.Data, wildcardPort); err != nil { + if err := InsertAllowRule(proxy.Iptables, proxy.GlobalBlocklist, answer.Data, wildcardPort); err != nil { WriteLog(fmt.Sprintf("Error setting firewall for wildcard domain %s: %v", domain, err)) } } @@ -238,7 +249,7 @@ func (proxy *DNSProxy) getIPByDomain(domain string) (string, error) { go WriteLog(fmt.Sprintf("domain resolved: %s, ip address: %s, TTL: %d", domain, answer.Data, answer.TTL)) - go proxy.ApiClient.sendDNSRecord(proxy.CorrelationId, proxy.Repo, domain, answer.Data) + go proxy.ApiClient.sendDNSRecord(proxy.CorrelationId, proxy.Repo, domain, answer.Data, "", "") go proxy.submitDNSEvent(answer.Data) diff --git a/dnsproxy_test.go b/dnsproxy_test.go index 1a4a3b5..34ce896 100644 --- a/dnsproxy_test.go +++ b/dnsproxy_test.go @@ -131,6 +131,7 @@ func TestDNSProxy_getResponse(t *testing.T) { Cache: tt.fields.Cache, ApiClient: apiclient, EgressPolicy: tt.fields.EgressPolicy, + GlobalBlocklist: NewGlobalBlocklist(nil), AllowedEndpoints: tt.fields.AllowedEndpoints, WildCardEndpoints: tt.fields.WildCardEndpoints, ReverseIPLookup: make(map[string]string), @@ -148,6 +149,42 @@ func TestDNSProxy_getResponse(t *testing.T) { } } +func TestDNSProxy_getResponse_GlobalBlocklist(t *testing.T) { + globalBlocklist := NewGlobalBlocklist(&GlobalBlocklistResponse{ + Domains: []CompromisedEndpoint{{Endpoint: "blocked.com", Reason: "compromised"}}, + WildcardDomains: []CompromisedEndpoint{{Endpoint: "*.blocked.example", Reason: "wildcard compromised"}}, + }) + + blockCache := InitCache(EgressPolicyBlock) + rrBlocked, _ := dns.NewRR(fmt.Sprintf("blocked.com. IN A %s", StepSecuritySinkHoleIPAddress)) + rrWildcardBlocked, _ := dns.NewRR(fmt.Sprintf("foo.blocked.example. IN A %s", StepSecuritySinkHoleIPAddress)) + + proxy := &DNSProxy{ + Cache: &blockCache, + ApiClient: &ApiClient{Client: &http.Client{}, APIURL: agentApiBaseUrl}, + EgressPolicy: EgressPolicyBlock, + GlobalBlocklist: globalBlocklist, + ReverseIPLookup: make(map[string]string), + Iptables: &Firewall{&MockIPTables{}}, + } + + response, err := proxy.getResponse(&dns.Msg{Question: []dns.Question{{Name: "blocked.com.", Qtype: dns.TypeA}}}) + if err != nil { + t.Fatalf("DNSProxy.getResponse() error = %v", err) + } + if !reflect.DeepEqual(response, &dns.Msg{Answer: []dns.RR{rrBlocked}}) { + t.Fatalf("DNSProxy.getResponse() = %v, want %v", response, &dns.Msg{Answer: []dns.RR{rrBlocked}}) + } + + response, err = proxy.getResponse(&dns.Msg{Question: []dns.Question{{Name: "foo.blocked.example.", Qtype: dns.TypeA}}}) + if err != nil { + t.Fatalf("DNSProxy.getResponse() error = %v", err) + } + if !reflect.DeepEqual(response, &dns.Msg{Answer: []dns.RR{rrWildcardBlocked}}) { + t.Fatalf("DNSProxy.getResponse() = %v, want %v", response, &dns.Msg{Answer: []dns.RR{rrWildcardBlocked}}) + } +} + func TestDNSProxy_auditCacheTTL(t *testing.T) { apiclient := &ApiClient{Client: &http.Client{}, APIURL: agentApiBaseUrl} @@ -167,6 +204,7 @@ func TestDNSProxy_auditCacheTTL(t *testing.T) { Cache: &cache, ApiClient: apiclient, EgressPolicy: EgressPolicyAudit, + GlobalBlocklist: NewGlobalBlocklist(nil), ReverseIPLookup: make(map[string]string), } diff --git a/eventhandler.go b/eventhandler.go index 4548ed0..c657c7f 100644 --- a/eventhandler.go +++ b/eventhandler.go @@ -36,7 +36,6 @@ type EventHandler struct { var classAPrivateSubnet, classBPrivateSubnet, classCPrivateSubnet, loopBackSubnet, ipv6LinkLocalSubnet, ipv6LocalSubnet *net.IPNet func (eventHandler *EventHandler) handleFileEvent(event *Event) { - eventHandler.fileMutex.Lock() if !strings.HasPrefix(event.FileName, "/") { event.FileName = path.Join(event.Path, event.FileName) @@ -62,11 +61,13 @@ func (eventHandler *EventHandler) handleFileEvent(event *Event) { // WriteLog(fmt.Sprintf("file write %s, syscall %s", event.FileName, event.Syscall)) if isSourceCodeFile(event.FileName) { + eventHandler.fileMutex.Lock() + defer eventHandler.fileMutex.Unlock() + _, found := eventHandler.SourceCodeMap[event.FileName] if !found { eventHandler.SourceCodeMap[event.FileName] = append(eventHandler.SourceCodeMap[event.FileName], event) - } - if found { + } else { isFromDifferentProcess := false for _, writeEvent := range eventHandler.SourceCodeMap[event.FileName] { if writeEvent.Pid != event.Pid { @@ -90,8 +91,6 @@ func (eventHandler *EventHandler) handleFileEvent(event *Event) { } } - eventHandler.fileMutex.Unlock() - eventHandler.submitFileEvent(event) } @@ -106,25 +105,21 @@ func isSourceCodeFile(fileName string) bool { func (eventHandler *EventHandler) handleProcessEvent(event *Event) { eventHandler.procMutex.Lock() - _, found := eventHandler.ProcessMap[event.Pid] - if !found { eventHandler.ProcessMap[event.Pid] = &Process{PID: event.Pid, PPid: event.PPid, Exe: event.Exe, Arguments: event.ProcessArguments} - eventHandler.procMutex.Unlock() + } + eventHandler.procMutex.Unlock() - if event.Euid == "0" { - image := eventHandler.GetContainerByPid(event.Pid) - if image == "" { - if event.Exe != "" { - if eventHandler.IsStartedByRunner(event.PPid, event.Exe) { - WriteLog(fmt.Sprintf("sudo process started: Exe: %s, Arguments: %v", event.Exe, event.ProcessArguments)) - } + if !found && event.Euid == "0" { + image := eventHandler.GetContainerByPid(event.Pid) + if image == "" { + if event.Exe != "" { + if eventHandler.IsStartedByRunner(event.PPid, event.Exe) { + WriteLog(fmt.Sprintf("sudo process started: Exe: %s, Arguments: %v", event.Exe, event.ProcessArguments)) } } } - } else { - eventHandler.procMutex.Unlock() } eventHandler.submitProcessEvent(event) @@ -168,7 +163,6 @@ func printContainerInfo(pid, ppid string) { }*/ func (eventHandler *EventHandler) handleNetworkEvent(event *Event) { - eventHandler.netMutex.Lock() if !isPrivateIPAddress(event.IPAddress) && // commenting out AzureIPAddress since it should not be called @@ -179,7 +173,12 @@ func (eventHandler *EventHandler) handleNetworkEvent(event *Event) { cacheKey := fmt.Sprintf("%s%s%s", event.Pid, event.IPAddress, event.Port) + eventHandler.netMutex.Lock() _, found := eventHandler.ProcessConnectionMap[cacheKey] + if !found { + eventHandler.ProcessConnectionMap[cacheKey] = true + } + eventHandler.netMutex.Unlock() if !found { tool := Tool{} @@ -193,18 +192,23 @@ func (eventHandler *EventHandler) handleNetworkEvent(event *Event) { tool = Tool{Name: image, SHA256: image} // TODO: Set container image checksum } reverseLookUp := eventHandler.DNSProxy.GetReverseIPLookup(event.IPAddress) - eventHandler.ApiClient.sendNetConnection(eventHandler.CorrelationId, eventHandler.Repo, event.IPAddress, event.Port, reverseLookUp, "", event.Timestamp, tool) + status := "" + matchedPolicy := "" + reason := "" + if eventHandler.DNSProxy.GlobalBlocklist != nil && eventHandler.DNSProxy.GlobalBlocklist.IsIPAddressBlocked(event.IPAddress) { + status = "Dropped" + matchedPolicy = GlobalBlocklistMatchedPolicy + reason = eventHandler.DNSProxy.GlobalBlocklist.BlockedIPAddressReason(event.IPAddress) + } + eventHandler.ApiClient.sendNetConnection(eventHandler.CorrelationId, eventHandler.Repo, event.IPAddress, event.Port, reverseLookUp, status, matchedPolicy, reason, event.Timestamp, tool) process := "" if image == "" { process = tool.Name } WriteLog(fmt.Sprintf("endpoint called ip address:port %s:%s, domain: %s, pid: %s, process: %s", event.IPAddress, event.Port, reverseLookUp, event.Pid, process)) - eventHandler.ProcessConnectionMap[cacheKey] = true } } - eventHandler.netMutex.Unlock() - eventHandler.submitNetworkEvent(event) } @@ -262,15 +266,15 @@ func (eventHandler *EventHandler) GetContainerByPid(pid string) string { procContainer := "" // see if already calculated - eventHandler.procMutex.Lock() + eventHandler.procMutex.RLock() process, found := eventHandler.ProcessMap[pid] if found { if process.Container != "" { - eventHandler.procMutex.Unlock() + eventHandler.procMutex.RUnlock() return process.Container } } - eventHandler.procMutex.Unlock() + eventHandler.procMutex.RUnlock() cgroupPath := fmt.Sprintf("/proc/%s/cgroup", pid) containerId := GetContainerIdByPid(cgroupPath) @@ -332,9 +336,9 @@ func (eventHandler *EventHandler) IsStartedByRunner(ppid, exe string) bool { } // In some cases the process has already exited, so get from map first - eventHandler.procMutex.Lock() + eventHandler.procMutex.RLock() parentProcess, found := eventHandler.ProcessMap[ppid] - eventHandler.procMutex.Unlock() + eventHandler.procMutex.RUnlock() if found { return eventHandler.IsStartedByRunner(parentProcess.PPid, parentProcess.Exe) @@ -360,9 +364,9 @@ func (eventHandler *EventHandler) GetToolChain(ppid, exe string) *Tool { tool := Tool{Name: filepath.Base(exe), SHA256: checksum} // In some cases the process has already exited, so get from map first - eventHandler.procMutex.Lock() + eventHandler.procMutex.RLock() parentProcess, found := eventHandler.ProcessMap[ppid] - eventHandler.procMutex.Unlock() + eventHandler.procMutex.RUnlock() if found { tool.Parent = eventHandler.GetToolChain(parentProcess.PPid, parentProcess.Exe) diff --git a/firewall.go b/firewall.go index 19c1fad..12d479a 100644 --- a/firewall.go +++ b/firewall.go @@ -19,11 +19,14 @@ const ( protocol = "-p" allProtocols = "all" tcp = "tcp" + udp = "udp" destination = "-d" destinationPort = "--dport" target = "-j" accept = "ACCEPT" reject = "REJECT" + nflogTarget = "NFLOG" + nflogGroup = "100" classAPrivateAddressRange = "10.0.0.0/8" classBPrivateAddressRange = "172.16.0.0/12" classCPrivateAddressRange = "192.168.0.0/16" @@ -178,7 +181,11 @@ func addBlockRules(firewall *Firewall, endpoints []ipAddressEndpoint, chain, net return nil } -func InsertAllowRule(firewall *Firewall, ipAddress, port string) error { +func InsertAllowRule(firewall *Firewall, blocklist *GlobalBlocklist, ipAddress, port string) error { + if blocklist != nil && blocklist.IsIPAddressBlocked(ipAddress) { + return nil + } + var ipt IPTables var err error if firewall == nil { @@ -231,6 +238,83 @@ func InsertAllowRule(firewall *Firewall, ipAddress, port string) error { return nil } +func AddGlobalBlockRules(firewall *Firewall, blocklist *GlobalBlocklist) error { + if blocklist == nil { + return nil + } + + if err := addGlobalBlockRulesForChain(firewall, blocklist, outputChain, defaultInterface, outbound); err != nil { + return errors.Wrap(err, "failed to add global block rules for default interface") + } + + if err := addGlobalBlockRulesForChain(firewall, blocklist, dockerUserChain, dockerInterface, inbound); err != nil { + return errors.Wrap(err, "failed to add global block rules for docker interface") + } + + return nil +} + +func addGlobalBlockRulesForChain(firewall *Firewall, blocklist *GlobalBlocklist, chain, netInterface, direction string) error { + var ipt IPTables + var err error + if firewall == nil { + ipt, err = iptables.New() + if err != nil { + return errors.Wrap(err, "new iptables failed") + } + } else { + ipt = firewall.IPTables + } + + // Add NFLOG rules once for all blocked IPs (TCP SYN + UDP) + tcpNflogExists, err := ipt.Exists(filterTable, chain, direction, netInterface, protocol, tcp, "--tcp-flags", "SYN,ACK", "SYN", target, nflogTarget, "--nflog-group", nflogGroup) + if err != nil { + return errors.Wrapf(err, "failed to check global block tcp nflog rule interface:%s", netInterface) + } + if !tcpNflogExists { + err = ipt.Insert(filterTable, chain, 1, direction, netInterface, protocol, tcp, "--tcp-flags", "SYN,ACK", "SYN", target, nflogTarget, "--nflog-group", nflogGroup) + if err != nil { + return errors.Wrapf(err, "failed to insert global block tcp nflog rule interface:%s", netInterface) + } + } + + udpNflogExists, err := ipt.Exists(filterTable, chain, direction, netInterface, protocol, udp, target, nflogTarget, "--nflog-group", nflogGroup) + if err != nil { + return errors.Wrapf(err, "failed to check global block udp nflog rule interface:%s", netInterface) + } + if !udpNflogExists { + err = ipt.Insert(filterTable, chain, 2, direction, netInterface, protocol, udp, target, nflogTarget, "--nflog-group", nflogGroup) + if err != nil { + return errors.Wrapf(err, "failed to insert global block udp nflog rule interface:%s", netInterface) + } + } + + for _, ipAddress := range blocklist.GetBlockedIPAddresses() { + if err := addGlobalBlockRule(ipt, chain, direction, netInterface, ipAddress); err != nil { + return err + } + } + + return nil +} + +func addGlobalBlockRule(ipt IPTables, chain, direction, netInterface, ipAddress string) error { + exists, err := ipt.Exists(filterTable, chain, direction, netInterface, destination, ipAddress, target, reject) + if err != nil { + return errors.Wrapf(err, "failed to check global block rule ip:%s, interface:%s", ipAddress, netInterface) + } + if exists { + return nil + } + + err = ipt.Insert(filterTable, chain, 3, direction, netInterface, destination, ipAddress, target, reject) + if err != nil { + return errors.Wrapf(err, "failed to insert global block rule ip:%s, interface:%s", ipAddress, netInterface) + } + + return nil +} + func AddAuditRules(firewall *Firewall) error { var ipt IPTables var err error diff --git a/firewall_blocklist_test.go b/firewall_blocklist_test.go new file mode 100644 index 0000000..b40b2e6 --- /dev/null +++ b/firewall_blocklist_test.go @@ -0,0 +1,107 @@ +package main + +import "testing" + +type recorderIPTables struct { + inserted [][]string +} + +func (m *recorderIPTables) Append(table, chain string, rulespec ...string) error { + return nil +} + +func (m *recorderIPTables) Exists(table, chain string, rulespec ...string) (bool, error) { + return false, nil +} + +func (m *recorderIPTables) Insert(table, chain string, pos int, rulespec ...string) error { + record := append([]string{table, chain}, rulespec...) + m.inserted = append(m.inserted, record) + return nil +} + +func (m *recorderIPTables) ClearChain(table, chain string) error { + return nil +} + +func insertedRuleTarget(record []string) string { + for i := 0; i < len(record)-1; i++ { + if record[i] == target { + return record[i+1] + } + } + + return "" +} + +func TestAddGlobalBlockRules(t *testing.T) { + ipt := &recorderIPTables{} + blocklist := NewGlobalBlocklist(&GlobalBlocklistResponse{ + IPAddresses: []CompromisedEndpoint{{Endpoint: "1.2.3.4", Reason: "compromised"}}, + }) + + err := AddGlobalBlockRules(&Firewall{ipt}, blocklist) + if err != nil { + t.Fatalf("AddGlobalBlockRules() error = %v", err) + } + + if len(ipt.inserted) != 6 { + t.Fatalf("expected 6 inserted rules, got %d", len(ipt.inserted)) + } + + expectedTargets := []string{nflogTarget, nflogTarget, reject, nflogTarget, nflogTarget, reject} + for i, targetName := range expectedTargets { + record := ipt.inserted[i] + if insertedRuleTarget(record) != targetName { + t.Fatalf("expected inserted rule %d target %s, got %#v", i, targetName, record) + } + } +} + +func TestAddGlobalBlockRules_WithNilBlocklist(t *testing.T) { + ipt := &recorderIPTables{} + err := AddGlobalBlockRules(&Firewall{ipt}, nil) + if err != nil { + t.Fatalf("AddGlobalBlockRules() error = %v", err) + } + + if len(ipt.inserted) != 0 { + t.Fatalf("expected no inserted rules, got %d", len(ipt.inserted)) + } +} + +func TestInsertAllowRule_SkipsGlobalBlocklistedIP(t *testing.T) { + blocklist := NewGlobalBlocklist(&GlobalBlocklistResponse{ + IPAddresses: []CompromisedEndpoint{{Endpoint: "1.2.3.4", Reason: "compromised"}}, + }) + + ipt := &recorderIPTables{} + + err := InsertAllowRule(&Firewall{ipt}, blocklist, "1.2.3.4", "443") + if err != nil { + t.Fatalf("InsertAllowRule() error = %v", err) + } + + if len(ipt.inserted) != 0 { + t.Fatalf("expected no inserted allow rules, got %d", len(ipt.inserted)) + } +} + +func TestInsertAllowRule_AllowsWhenBlocklistIsNil(t *testing.T) { + ipt := &recorderIPTables{} + + err := InsertAllowRule(&Firewall{ipt}, nil, "1.2.3.4", "443") + if err != nil { + t.Fatalf("InsertAllowRule() error = %v", err) + } + + if len(ipt.inserted) != 2 { + t.Fatalf("expected two inserted allow rules, got %d", len(ipt.inserted)) + } + + for i, record := range ipt.inserted { + if insertedRuleTarget(record) != accept { + t.Fatalf("expected inserted rule %d target %s, got %#v", i, accept, record) + } + } +} diff --git a/global_blocklist.go b/global_blocklist.go new file mode 100644 index 0000000..7e30c81 --- /dev/null +++ b/global_blocklist.go @@ -0,0 +1,156 @@ +package main + +import ( + "strings" + + "github.com/miekg/dns" +) + +const GlobalBlocklistMatchedPolicy = "GLOBAL_BLOCKLIST" + +type CompromisedEndpoint struct { + Endpoint string `json:"endpoint"` + Reason string `json:"reason,omitempty"` +} + +type GlobalBlocklistResponse struct { + IPAddresses []CompromisedEndpoint `json:"ip_addresses,omitempty"` + Domains []CompromisedEndpoint `json:"domains,omitempty"` + WildcardDomains []CompromisedEndpoint `json:"wildcard_domains,omitempty"` +} + +type GlobalBlocklist struct { + ipAddresses map[string]string + domains map[string]string + wildcardDomains map[string]string +} + +func NewGlobalBlocklist(response *GlobalBlocklistResponse) *GlobalBlocklist { + blocklist := &GlobalBlocklist{ + ipAddresses: make(map[string]string), + domains: make(map[string]string), + wildcardDomains: make(map[string]string), + } + + if response == nil { + return blocklist + } + + for _, endpoint := range response.IPAddresses { + ipAddress := strings.TrimSpace(strings.ToLower(endpoint.Endpoint)) + if ipAddress != "" { + blocklist.ipAddresses[ipAddress] = endpoint.Reason + } + } + + for _, endpoint := range response.Domains { + domain := normalizeBlocklistDomain(endpoint.Endpoint) + if domain != "" { + blocklist.domains[domain] = endpoint.Reason + } + } + + for _, endpoint := range response.WildcardDomains { + domain := normalizeWildcardBlocklistDomain(endpoint.Endpoint) + if domain != "" { + blocklist.wildcardDomains[domain] = endpoint.Reason + } + } + + return blocklist +} + +func (blocklist *GlobalBlocklist) IsIPAddressBlocked(ipAddress string) bool { + if blocklist == nil { + return false + } + + _, found := blocklist.ipAddresses[strings.TrimSpace(strings.ToLower(ipAddress))] + return found +} + +func (blocklist *GlobalBlocklist) BlockedIPAddressReason(ipAddress string) string { + if blocklist == nil { + return "" + } + + return blocklist.ipAddresses[strings.TrimSpace(strings.ToLower(ipAddress))] +} + +func (blocklist *GlobalBlocklist) GetBlockedIPAddresses() []string { + if blocklist == nil { + return nil + } + + ipAddresses := make([]string, 0, len(blocklist.ipAddresses)) + for ipAddress := range blocklist.ipAddresses { + ipAddresses = append(ipAddresses, ipAddress) + } + + return ipAddresses +} + +func (blocklist *GlobalBlocklist) IsDomainBlocked(domain string) (bool, string) { + if blocklist == nil { + return false, "" + } + + normalizedDomain := normalizeBlocklistDomain(domain) + if normalizedDomain == "" { + return false, "" + } + + if reason, found := blocklist.domains[normalizedDomain]; found { + return true, reason + } + + return blocklist.matchesBlockedWildcardDomain(normalizedDomain) +} + +func (blocklist *GlobalBlocklist) BlockedDomainReason(domain string) string { + _, reason := blocklist.IsDomainBlocked(domain) + return reason +} + +func (blocklist *GlobalBlocklist) IsWildcardDomainBlocked(domain string) (bool, string) { + if blocklist == nil { + return false, "" + } + + normalizedDomain := normalizeWildcardBlocklistDomain(domain) + if normalizedDomain == "" { + return false, "" + } + + for wildcardDomain, reason := range blocklist.wildcardDomains { + if normalizedDomain == wildcardDomain || strings.HasSuffix(normalizedDomain, "."+wildcardDomain) { + return true, reason + } + } + + return false, "" +} + +func (blocklist *GlobalBlocklist) matchesBlockedWildcardDomain(normalizedDomain string) (bool, string) { + for wildcardDomain, reason := range blocklist.wildcardDomains { + if strings.HasSuffix(normalizedDomain, "."+wildcardDomain) { + return true, reason + } + } + + return false, "" +} + +func normalizeBlocklistDomain(domain string) string { + domain = strings.TrimSpace(strings.ToLower(domain)) + if domain == "" { + return "" + } + + return dns.Fqdn(domain) +} + +func normalizeWildcardBlocklistDomain(domain string) string { + domain = normalizeBlocklistDomain(domain) + return strings.TrimPrefix(domain, "*.") +} diff --git a/netmon.go b/netmon.go index ce482fd..6188a90 100644 --- a/netmon.go +++ b/netmon.go @@ -15,14 +15,15 @@ import ( const Unknown = "Unknown" type NetworkMonitor struct { - CorrelationId string - Repo string - ApiClient *ApiClient - Status string - netMutex sync.RWMutex + CorrelationId string + Repo string + ApiClient *ApiClient + GlobalBlocklist *GlobalBlocklist + Status string + netMutex sync.RWMutex } -var ipAddresses = make(map[string]int) +var ipAddresses = make(map[string]bool) func (netMonitor *NetworkMonitor) MonitorNetwork(ctx context.Context, nflogger AgentNflogger, errc chan error) []string { @@ -92,17 +93,30 @@ func (netMonitor *NetworkMonitor) handlePacket(attrs nflog.Attribute) { ipv4, _ := ipv4Layer.(*layers.IPv4) netMonitor.netMutex.Lock() ipv4Address := ipv4.DstIP.String() - _, found := ipAddresses[ipv4Address] + matchedPolicy := "" + reason := "" + status := netMonitor.Status + if netMonitor.GlobalBlocklist != nil && netMonitor.GlobalBlocklist.IsIPAddressBlocked(ipv4Address) { + status = "Dropped" + matchedPolicy = GlobalBlocklistMatchedPolicy + reason = netMonitor.GlobalBlocklist.BlockedIPAddressReason(ipv4Address) + } + + cacheKey := fmt.Sprintf("%s:%s:%s", ipv4Address, port, status) + _, found := ipAddresses[cacheKey] if !found { - ipAddresses[ipv4Address] = 1 + ipAddresses[cacheKey] = true if isSYN || isUDP { - if netMonitor.Status == "Dropped" { - + if status == "Dropped" { netMonitor.ApiClient.sendNetConnection(netMonitor.CorrelationId, netMonitor.Repo, - ipv4Address, port, "", netMonitor.Status, timestamp, Tool{Name: Unknown, SHA256: Unknown}) + ipv4Address, port, "", status, matchedPolicy, reason, timestamp, Tool{Name: Unknown, SHA256: Unknown}) - go WriteLog(fmt.Sprintf("ip address dropped: %s", ipv4Address)) + logMessage := fmt.Sprintf("ip address dropped: %s", ipv4Address) + if reason != "" { + logMessage = fmt.Sprintf("%s, reason: %s", logMessage, reason) + } + go WriteLog(logMessage) if ipv4Address != StepSecuritySinkHoleIPAddress { // Sinkhole IP address will be covered by DNS block go WriteAnnotation(fmt.Sprintf("StepSecurity Harden Runner: Traffic to IP Address %s was blocked", ipv4Address)) From 9564f9e1e382fa23d787a8d9f73d55577abad3e1 Mon Sep 17 00:00:00 2001 From: Jatin <84621253+h0x0er@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:47:49 +0530 Subject: [PATCH 14/25] dont send net-connection for sinkhole ip, bump armour and go-version * dont send net-connection for sinkhole ip * bump armour to v1.2.4 * armour: bump to v1.4.0 * bump go version to 1.26.2 --- .github/workflows/int.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- eventhandler.go | 17 ++++++++--------- go.mod | 16 ++++++++-------- go.sum | 34 ++++++++++++++++------------------ 6 files changed, 35 insertions(+), 38 deletions(-) diff --git a/.github/workflows/int.yml b/.github/workflows/int.yml index e7f7cc8..c33cf43 100644 --- a/.github/workflows/int.yml +++ b/.github/workflows/int.yml @@ -23,7 +23,7 @@ jobs: - name: Set up Go uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4 with: - go-version: 1.24.1 + go-version: 1.26.2 - name: Configure .netrc run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fec7a51..902ab60 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: - name: Set up Go uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4 with: - go-version: 1.24.1 + go-version: 1.26.2 - name: Configure .netrc run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 542388e..6dd66b5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Go uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4 with: - go-version: 1.24.1 + go-version: 1.26.2 - name: Configure .netrc run: | diff --git a/eventhandler.go b/eventhandler.go index c657c7f..1051338 100644 --- a/eventhandler.go +++ b/eventhandler.go @@ -164,6 +164,11 @@ func printContainerInfo(pid, ppid string) { func (eventHandler *EventHandler) handleNetworkEvent(event *Event) { + // sinkhole is returned in-case of dns-block + if event.IPAddress == StepSecuritySinkHoleIPAddress { + return + } + if !isPrivateIPAddress(event.IPAddress) && // commenting out AzureIPAddress since it should not be called // strings.Compare(event.IPAddress, AzureIPAddress) != 0 && @@ -191,16 +196,10 @@ func (eventHandler *EventHandler) handleNetworkEvent(event *Event) { } else { tool = Tool{Name: image, SHA256: image} // TODO: Set container image checksum } + reverseLookUp := eventHandler.DNSProxy.GetReverseIPLookup(event.IPAddress) - status := "" - matchedPolicy := "" - reason := "" - if eventHandler.DNSProxy.GlobalBlocklist != nil && eventHandler.DNSProxy.GlobalBlocklist.IsIPAddressBlocked(event.IPAddress) { - status = "Dropped" - matchedPolicy = GlobalBlocklistMatchedPolicy - reason = eventHandler.DNSProxy.GlobalBlocklist.BlockedIPAddressReason(event.IPAddress) - } - eventHandler.ApiClient.sendNetConnection(eventHandler.CorrelationId, eventHandler.Repo, event.IPAddress, event.Port, reverseLookUp, status, matchedPolicy, reason, event.Timestamp, tool) + eventHandler.ApiClient.sendNetConnection(eventHandler.CorrelationId, eventHandler.Repo, event.IPAddress, event.Port, reverseLookUp, "", "", "", event.Timestamp, tool) + process := "" if image == "" { process = tool.Name diff --git a/go.mod b/go.mod index feee67c..267f570 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/step-security/agent -go 1.24.6 +go 1.26.2 require ( github.com/coreos/go-iptables v0.6.0 @@ -10,7 +10,7 @@ require ( github.com/jarcoal/httpmock v1.3.0 github.com/miekg/dns v1.1.57 github.com/pkg/errors v0.9.1 - github.com/step-security/armour v1.2.1 + github.com/step-security/armour v1.4.0 ) require ( @@ -67,10 +67,10 @@ require ( go.opentelemetry.io/otel/trace v1.40.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/tools v0.38.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/tools v0.44.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -82,6 +82,6 @@ require ( github.com/docker/docker v23.0.4+incompatible github.com/google/go-cmp v0.7.0 // indirect github.com/mdlayher/netlink v1.7.2 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sys v0.40.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sys v0.43.0 // indirect ) diff --git a/go.sum b/go.sum index 86e9aec..6c04225 100644 --- a/go.sum +++ b/go.sum @@ -187,10 +187,8 @@ github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0= github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/step-security/armour v1.2.0 h1:DzGsX0HOWiMJpD8wo5k5bJKohWbjk3WYm6x13UNzsTE= -github.com/step-security/armour v1.2.0/go.mod h1:uopgPKW3HfuP05Wc0QeaN9buiy/XJH2B5HdNuM8sBLE= -github.com/step-security/armour v1.2.1 h1:WFhrnUanh60c+jUXBsEX01jS5pi51Cs28wq//tLiuqU= -github.com/step-security/armour v1.2.1/go.mod h1:uopgPKW3HfuP05Wc0QeaN9buiy/XJH2B5HdNuM8sBLE= +github.com/step-security/armour v1.4.0 h1:vf2P1Ly764hmV/h38MmYQxZMwIPEpeGWOwLKXsKnfg0= +github.com/step-security/armour v1.4.0/go.mod h1:pi8hNRxD8BDgd2pNOGH7YW+pbUDf0NW3k4IBrBiwGYc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -247,14 +245,14 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -270,13 +268,13 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -298,14 +296,14 @@ golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -313,8 +311,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 0688505ebaa67921e499518b1f586682b8a5c913 Mon Sep 17 00:00:00 2001 From: Jatin <84621253+h0x0er@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:41:06 +0530 Subject: [PATCH 15/25] don't net-conn event for sinkhole-ip as this was causing issue in generation of dummyNetworkEvent incase a domain was blocked. enabled setting to protect agent from getting killed --- agent.go | 7 +++++++ global_feature_flags.go | 1 + netmon.go | 19 +++++++++++-------- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/agent.go b/agent.go index 5c26893..07c1d4a 100644 --- a/agent.go +++ b/agent.go @@ -80,6 +80,7 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, InitGlobalFeatureFlags(config.APIURL, apiclient) WriteLog("initialized global feature flags") WriteLog("\n") + globalFlags := GetGlobalFeatureFlags() globalBlocklistResponse, err := apiclient.getGlobalBlocklist() globalBlocklist := NewGlobalBlocklist(globalBlocklistResponse) @@ -253,6 +254,12 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, Pids: getPidsOfInterest(), Files: []string{}, EnforceReadBlock: false, + EnforceKillBlock: func() bool { + if globalFlags.DisableKillEnforcement { + return false + } + return true + }(), ApiConf: &armour.ApiConf{ APIURL: config.APIURL, Repo: config.Repo, diff --git a/global_feature_flags.go b/global_feature_flags.go index f760a73..81201aa 100644 --- a/global_feature_flags.go +++ b/global_feature_flags.go @@ -14,6 +14,7 @@ type GlobalFeatureFlags struct { AgentType string `json:"agent_type"` EnableArmour bool `json:"enable_armour"` EnableCustomDetectionRules bool `json:"enable_custom_detection_rules"` + DisableKillEnforcement bool `json:"disable_kill_enforcement,omitempty"` } // GlobalFeatureFlagManager manages fetching and caching of global feature flags. diff --git a/netmon.go b/netmon.go index 6188a90..b0be672 100644 --- a/netmon.go +++ b/netmon.go @@ -109,16 +109,19 @@ func (netMonitor *NetworkMonitor) handlePacket(attrs nflog.Attribute) { if isSYN || isUDP { if status == "Dropped" { - netMonitor.ApiClient.sendNetConnection(netMonitor.CorrelationId, netMonitor.Repo, - ipv4Address, port, "", status, matchedPolicy, reason, timestamp, Tool{Name: Unknown, SHA256: Unknown}) - - logMessage := fmt.Sprintf("ip address dropped: %s", ipv4Address) - if reason != "" { - logMessage = fmt.Sprintf("%s, reason: %s", logMessage, reason) - } - go WriteLog(logMessage) if ipv4Address != StepSecuritySinkHoleIPAddress { // Sinkhole IP address will be covered by DNS block + + netMonitor.ApiClient.sendNetConnection(netMonitor.CorrelationId, netMonitor.Repo, + ipv4Address, port, "", status, matchedPolicy, reason, timestamp, Tool{Name: Unknown, SHA256: Unknown}) + + logMessage := fmt.Sprintf("ip address dropped: %s", ipv4Address) + if reason != "" { + logMessage = fmt.Sprintf("%s, reason: %s", logMessage, reason) + } + + go WriteLog(logMessage) + go WriteAnnotation(fmt.Sprintf("StepSecurity Harden Runner: Traffic to IP Address %s was blocked", ipv4Address)) } } From 0e00c9758a3b54fa4dd9bc730cc3240d315f8716 Mon Sep 17 00:00:00 2001 From: Jatin <84621253+h0x0er@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:08:09 +0530 Subject: [PATCH 16/25] resovled comments --- agent.go | 7 +------ netmon.go | 32 +++++++++++++++++--------------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/agent.go b/agent.go index 07c1d4a..acee4eb 100644 --- a/agent.go +++ b/agent.go @@ -254,12 +254,7 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, Pids: getPidsOfInterest(), Files: []string{}, EnforceReadBlock: false, - EnforceKillBlock: func() bool { - if globalFlags.DisableKillEnforcement { - return false - } - return true - }(), + EnforceKillBlock: !globalFlags.DisableKillEnforcement, ApiConf: &armour.ApiConf{ APIURL: config.APIURL, Repo: config.Repo, diff --git a/netmon.go b/netmon.go index b0be672..44edcee 100644 --- a/netmon.go +++ b/netmon.go @@ -104,30 +104,32 @@ func (netMonitor *NetworkMonitor) handlePacket(attrs nflog.Attribute) { cacheKey := fmt.Sprintf("%s:%s:%s", ipv4Address, port, status) _, found := ipAddresses[cacheKey] - if !found { - ipAddresses[cacheKey] = true + if found { + netMonitor.netMutex.Unlock() + return + } + ipAddresses[cacheKey] = true + netMonitor.netMutex.Unlock() - if isSYN || isUDP { - if status == "Dropped" { + if isSYN || isUDP { + if status == "Dropped" { - if ipv4Address != StepSecuritySinkHoleIPAddress { // Sinkhole IP address will be covered by DNS block + if ipv4Address != StepSecuritySinkHoleIPAddress { // Sinkhole IP address will be covered by DNS block - netMonitor.ApiClient.sendNetConnection(netMonitor.CorrelationId, netMonitor.Repo, - ipv4Address, port, "", status, matchedPolicy, reason, timestamp, Tool{Name: Unknown, SHA256: Unknown}) + netMonitor.ApiClient.sendNetConnection(netMonitor.CorrelationId, netMonitor.Repo, + ipv4Address, port, "", status, matchedPolicy, reason, timestamp, Tool{Name: Unknown, SHA256: Unknown}) - logMessage := fmt.Sprintf("ip address dropped: %s", ipv4Address) - if reason != "" { - logMessage = fmt.Sprintf("%s, reason: %s", logMessage, reason) - } + logMessage := fmt.Sprintf("ip address dropped: %s", ipv4Address) + if reason != "" { + logMessage = fmt.Sprintf("%s, reason: %s", logMessage, reason) + } - go WriteLog(logMessage) + go WriteLog(logMessage) - go WriteAnnotation(fmt.Sprintf("StepSecurity Harden Runner: Traffic to IP Address %s was blocked", ipv4Address)) - } + go WriteAnnotation(fmt.Sprintf("StepSecurity Harden Runner: Traffic to IP Address %s was blocked", ipv4Address)) } } } - netMonitor.netMutex.Unlock() } } From fad6747164e49d9bbb150a5522c445e2757a287f Mon Sep 17 00:00:00 2001 From: Rohan Prabhu Date: Fri, 21 Aug 2026 22:38:01 +0530 Subject: [PATCH 17/25] fix: revoke sudo before Docker teardown Call disableSudoAndContainers early in agent startup and revoke sudo and socket permissions before the slow container cleanup, which now runs in the background. Add tests for revocation order and socket perms. (cherry picked from commit aa706cdc4b0d3e90dea01d64ef146bad00befada) --- agent.go | 29 ++++---- sudo.go | 55 ++++++++------ sudo_test.go | 205 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 254 insertions(+), 35 deletions(-) create mode 100644 sudo_test.go diff --git a/agent.go b/agent.go index acee4eb..e254dd3 100644 --- a/agent.go +++ b/agent.go @@ -113,6 +113,21 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, } } + sudo := Sudo{} + + // Revoke sudo and container access as early as possible — only the parsed + // config and temp dir are prerequisites. Container teardown runs in the + // background inside disableSudoAndContainers; job steps must not get a + // window to run `sudo` while DNS/firewall setup is still in progress. + if config.DisableSudoAndContainers { + err := sudo.disableSudoAndContainers(tempDir) + if err != nil { + WriteLog(fmt.Sprintf("%s Unable to disable sudo and docker %v", StepSecurityAnnotationPrefix, err)) + } else { + WriteLog("disabled sudo and docker") + } + } + Cache := InitCache(config.EgressPolicy) allowedEndpoints, wildcardEndpoints := addImplicitEndpoints(config.Endpoints, config.DisableTelemetry, globalBlocklist) @@ -143,13 +158,8 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, } dnsConfig := DnsConfig{} - sudo := Sudo{} var ipAddressEndpoints []ipAddressEndpoint - if config.DisableSudoAndContainers { - go sudo.uninstallDocker() - } - // hydrate dns cache if config.EgressPolicy == EgressPolicyBlock { for domainName, endpoints := range allowedEndpoints { @@ -283,15 +293,6 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, } } - if config.DisableSudoAndContainers { - err := sudo.disableSudoAndContainers(tempDir) - if err != nil { - WriteLog(fmt.Sprintf("%s Unable to disable sudo and docker %v", StepSecurityAnnotationPrefix, err)) - } else { - WriteLog("disabled sudo and docker") - } - } - if config.DisableSudo { err := sudo.disableSudo(tempDir) if err != nil { diff --git a/sudo.go b/sudo.go index 80ca83b..924282b 100644 --- a/sudo.go +++ b/sudo.go @@ -13,10 +13,17 @@ type Sudo struct { SudoersBackUpPath string } -const ( - sudoersFile = "/etc/sudoers.d/runner" - runnerUser = "runner" -) +// sudoersFile is a var so tests can point it at a temp file +var sudoersFile = "/etc/sudoers.d/runner" + +const runnerUser = "runner" + +// runCmd is a var so tests can inject a fake command runner +var runCmd = run + +// socket paths are vars so tests can point them at temp files +var dockerSockPath = "/var/run/docker.sock" +var containerdSockPath = "/run/containerd/containerd.sock" func (s *Sudo) disableSudo(tempDir string) error { s.SudoersBackUpPath = path.Join(tempDir, "runner") @@ -47,18 +54,28 @@ func (s *Sudo) revertDisableSudo() error { func (s *Sudo) disableSudoAndContainers(tempDir string) error { - s.removeDockerDirectoriesAndFiles() - - // Remove socket permissions if they exist - s.removeSocketPermissions() var errstrings []string + // Revoke sudo first: a single truncate syscall. This must happen before + // any container teardown — the teardown takes several seconds and job + // steps can start (and run `sudo`) while it is still in progress. err := s.disableSudo(tempDir) if err != nil { WriteLog(fmt.Sprintf("error disabling sudo: %v", err)) errstrings = append(errstrings, err.Error()) } + // Revoke container access second: chmod on the sockets, also instant. + s.removeSocketPermissions() + + // Slow cleanup runs in the background — it is not enforcement and must + // not delay it. Purge first (stops the daemon, unmounts overlays), then + // delete the directories; racing the two makes the delete far slower. + go func() { + s.uninstallDocker() + s.removeDockerDirectoriesAndFiles() + }() + //flatten errs if len(errstrings) > 0 { return fmt.Errorf("error disabling sudo and containers: %s", strings.Join(errstrings, "\n")) @@ -69,19 +86,15 @@ func (s *Sudo) disableSudoAndContainers(tempDir string) error { // removeSocketPermissions removes permissions from Docker and containerd sockets if they exist func (s *Sudo) removeSocketPermissions() { // Check and remove docker.sock permissions if it exists - if _, err := os.Stat("/var/run/docker.sock"); err == nil { - cmd := exec.Command("sudo", "chmod", "000", "/var/run/docker.sock") - err := cmd.Run() - if err != nil { + if _, err := os.Stat(dockerSockPath); err == nil { + if err := os.Chmod(dockerSockPath, 0000); err != nil { WriteLog(fmt.Sprintf("error removing docker.sock permissions: %v", err)) } } // Check and remove containerd.sock permissions if it exists - if _, err := os.Stat("/run/containerd/containerd.sock"); err == nil { - cmd := exec.Command("sudo", "chmod", "000", "/run/containerd/containerd.sock") - err := cmd.Run() - if err != nil { + if _, err := os.Stat(containerdSockPath); err == nil { + if err := os.Chmod(containerdSockPath, 0000); err != nil { WriteLog(fmt.Sprintf("error removing containerd.sock permissions: %v", err)) } } @@ -120,15 +133,15 @@ func run(cmd string, args ...string) { func (s *Sudo) uninstallDocker() error { WriteLog("Uninstalling docker") - run("sudo", "apt-get", "purge", "-y", + runCmd("apt-get", "purge", "-y", "docker-ce", "docker-ce-cli", "containerd.io") return nil } func (s *Sudo) removeDockerDirectoriesAndFiles() error { - run("sudo", "rm", "-rf", "/var/lib/docker") - run("sudo", "rm", "-rf", "/var/lib/containerd") - run("sudo", "rm", "-f", "/etc/apt/sources.list.d/docker.list") - run("sudo", "rm", "-f", "/etc/apt/keyrings/docker.asc") + runCmd("rm", "-rf", "/var/lib/docker") + runCmd("rm", "-rf", "/var/lib/containerd") + runCmd("rm", "-f", "/etc/apt/sources.list.d/docker.list") + runCmd("rm", "-f", "/etc/apt/keyrings/docker.asc") return nil } diff --git a/sudo_test.go b/sudo_test.go new file mode 100644 index 0000000..3cd7202 --- /dev/null +++ b/sudo_test.go @@ -0,0 +1,205 @@ +package main + +import ( + "os" + "path" + "strings" + "sync" + "testing" + "time" +) + +const testSudoersContent = "runner ALL=(ALL) NOPASSWD:ALL\n" + +func setupFakeSudoers(t *testing.T) string { + t.Helper() + origSudoers := sudoersFile + sudoersFile = path.Join(t.TempDir(), "runner") + t.Cleanup(func() { sudoersFile = origSudoers }) + + redirectSocketPaths(t) + // 0640 rather than the real file's 0440: the agent truncates as root + // (which bypasses permission checks), but tests run unprivileged. + if err := os.WriteFile(sudoersFile, []byte(testSudoersContent), 0640); err != nil { + t.Fatalf("failed to create fake sudoers file: %v", err) + } + return sudoersFile +} + +// redirectSocketPaths points socket paths at non-existent temp files so tests +// never chmod the machine's real docker/containerd sockets. +func redirectSocketPaths(t *testing.T) { + t.Helper() + origDocker, origContainerd := dockerSockPath, containerdSockPath + dockerSockPath = path.Join(t.TempDir(), "no-such-docker.sock") + containerdSockPath = path.Join(t.TempDir(), "no-such-containerd.sock") + t.Cleanup(func() { dockerSockPath, containerdSockPath = origDocker, origContainerd }) +} + +func Test_disableSudoAndContainers_RevokesSudoBeforeContainerCleanup(t *testing.T) { + setupFakeSudoers(t) + + type observed struct { + command string + sudoersSize int64 + } + + firstCmd := make(chan observed, 1) + release := make(chan struct{}) + var mu sync.Mutex + var commands []string + + origRun := runCmd + runCmd = func(cmd string, args ...string) { + full := strings.Join(append([]string{cmd}, args...), " ") + var size int64 = -1 + if fi, err := os.Stat(sudoersFile); err == nil { + size = fi.Size() + } + mu.Lock() + commands = append(commands, full) + mu.Unlock() + select { + case firstCmd <- observed{command: full, sudoersSize: size}: + default: + } + <-release + } + t.Cleanup(func() { + close(release) + runCmd = origRun + }) + + sudo := &Sudo{} + done := make(chan error, 1) + go func() { + done <- sudo.disableSudoAndContainers(t.TempDir()) + }() + + // Sudo revocation must not wait on container teardown, which is + // simulated here by a command runner that blocks forever. + select { + case err := <-done: + if err != nil { + t.Fatalf("disableSudoAndContainers returned error: %v", err) + } + case <-time.After(2 * time.Second): + t.Fatal("disableSudoAndContainers blocked on container teardown; sudo revocation must complete first") + } + + fi, err := os.Stat(sudoersFile) + if err != nil { + t.Fatalf("failed to stat sudoers file: %v", err) + } + if fi.Size() != 0 { + t.Fatalf("sudoers file not truncated after disableSudoAndContainers returned; size = %d", fi.Size()) + } + + // The first container-cleanup command must observe an already-truncated + // sudoers file, and must be the package purge (daemon stop) rather than + // a directory delete. + select { + case obs := <-firstCmd: + if obs.sudoersSize != 0 { + t.Fatalf("first container-cleanup command %q ran before sudoers truncate; observed size = %d", obs.command, obs.sudoersSize) + } + if !strings.HasPrefix(obs.command, "apt-get purge") { + t.Fatalf("first container-cleanup command = %q, want package purge before directory deletion", obs.command) + } + case <-time.After(2 * time.Second): + t.Fatal("no container-cleanup command observed") + } +} + +func Test_disableSudoAndContainers_ErrorSurfacesWhenSudoersMissing(t *testing.T) { + origSudoers := sudoersFile + sudoersFile = path.Join(t.TempDir(), "does-not-exist") + t.Cleanup(func() { sudoersFile = origSudoers }) + redirectSocketPaths(t) + + origRun := runCmd + runCmd = func(cmd string, args ...string) {} + t.Cleanup(func() { runCmd = origRun }) + + sudo := &Sudo{} + err := sudo.disableSudoAndContainers(t.TempDir()) + if err == nil { + t.Fatal("expected error when sudoers backup fails, got nil") + } + if !strings.Contains(err.Error(), "error disabling sudo and containers") { + t.Fatalf("error = %q, want it to contain %q", err.Error(), "error disabling sudo and containers") + } +} + +// Socket permissions must be revoked synchronously (before return), via chmod. +func Test_disableSudoAndContainers_RemovesSocketPermissions(t *testing.T) { + setupFakeSudoers(t) + + sockDir := t.TempDir() + origDocker, origContainerd := dockerSockPath, containerdSockPath + dockerSockPath = path.Join(sockDir, "docker.sock") + containerdSockPath = path.Join(sockDir, "containerd.sock") + t.Cleanup(func() { dockerSockPath, containerdSockPath = origDocker, origContainerd }) + + for _, p := range []string{dockerSockPath, containerdSockPath} { + if err := os.WriteFile(p, nil, 0660); err != nil { + t.Fatalf("failed to create fake socket %s: %v", p, err) + } + } + + origRun := runCmd + runCmd = func(cmd string, args ...string) {} + t.Cleanup(func() { runCmd = origRun }) + + sudo := &Sudo{} + if err := sudo.disableSudoAndContainers(t.TempDir()); err != nil { + t.Fatalf("disableSudoAndContainers returned error: %v", err) + } + + for _, p := range []string{dockerSockPath, containerdSockPath} { + fi, err := os.Stat(p) + if err != nil { + t.Fatalf("failed to stat %s: %v", p, err) + } + if fi.Mode().Perm() != 0 { + t.Fatalf("%s permissions not removed: %v", p, fi.Mode().Perm()) + } + } +} + +func Test_disableSudo_BackupAndRevert(t *testing.T) { + setupFakeSudoers(t) + + sudo := &Sudo{} + tempDir := t.TempDir() + if err := sudo.disableSudo(tempDir); err != nil { + t.Fatalf("disableSudo returned error: %v", err) + } + + backup, err := os.ReadFile(sudo.SudoersBackUpPath) + if err != nil { + t.Fatalf("failed to read backup: %v", err) + } + if string(backup) != testSudoersContent { + t.Fatalf("backup content = %q, want %q", string(backup), testSudoersContent) + } + + fi, err := os.Stat(sudoersFile) + if err != nil { + t.Fatalf("failed to stat sudoers file: %v", err) + } + if fi.Size() != 0 { + t.Fatalf("sudoers file not truncated; size = %d", fi.Size()) + } + + if err := sudo.revertDisableSudo(); err != nil { + t.Fatalf("revertDisableSudo returned error: %v", err) + } + restored, err := os.ReadFile(sudoersFile) + if err != nil { + t.Fatalf("failed to read restored sudoers file: %v", err) + } + if string(restored) != testSudoersContent { + t.Fatalf("restored content = %q, want %q", string(restored), testSudoersContent) + } +} From b7eb3ed548637cb4bc9a87bea0d6024c6bb6c5b8 Mon Sep 17 00:00:00 2001 From: Rohan Prabhu Date: Fri, 21 Aug 2026 22:56:12 +0530 Subject: [PATCH 18/25] feat: fetch GitHub Actions domains from meta API Fetch GitHub Actions endpoints from the GitHub meta API and add them to the implicit endpoint list, with retries on failure. Skip domains under githubusercontent.com because existing wildcard implicit endpoints already cover them. If the fetch fails, the hardcoded implicit endpoints remain as the baseline. (cherry picked from commit 5391cf1beaf94a5399792acc6ef2a2ca3d45d5d0) --- agent.go | 18 ++++++++- agent_test.go | 29 +++++++++++++- apiclient.go | 60 +++++++++++++++++++++++++++++ apiclient_test.go | 98 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 apiclient_test.go diff --git a/agent.go b/agent.go index e254dd3..152417a 100644 --- a/agent.go +++ b/agent.go @@ -130,7 +130,17 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, Cache := InitCache(config.EgressPolicy) - allowedEndpoints, wildcardEndpoints := addImplicitEndpoints(config.Endpoints, config.DisableTelemetry, globalBlocklist) + // Fetch GitHub Actions domains from the meta API. On failure the + // hardcoded implicit endpoints remain the baseline. + githubMetaDomains, err := apiclient.getGithubMetaDomains() + if err != nil { + WriteLog(fmt.Sprintf("Error fetching GitHub meta domains: %v", err)) + } else { + WriteLog(fmt.Sprintf("fetched GitHub meta domains: %+v", githubMetaDomains)) + WriteLog("\n") + } + + allowedEndpoints, wildcardEndpoints := addImplicitEndpoints(config.Endpoints, config.DisableTelemetry, globalBlocklist, githubMetaDomains) // Start DNS servers and get confirmation dnsProxy := DNSProxy{ @@ -372,7 +382,7 @@ func refreshDNSEntries(ctx context.Context, iptables *Firewall, blocklist *Globa } -func addImplicitEndpoints(endpoints map[string][]Endpoint, disableTelemetry bool, blocklist *GlobalBlocklist) (map[string][]Endpoint, map[string][]Endpoint) { +func addImplicitEndpoints(endpoints map[string][]Endpoint, disableTelemetry bool, blocklist *GlobalBlocklist, githubMetaDomains []Endpoint) (map[string][]Endpoint, map[string][]Endpoint) { normalEndpoints := make(map[string][]Endpoint) wildcardEndpoints := make(map[string][]Endpoint) @@ -385,6 +395,10 @@ func addImplicitEndpoints(endpoints map[string][]Endpoint, disableTelemetry bool {domainName: "productionresultssa*.blob.core.windows.net.", port: 443}, // GitHub } + // GitHub Actions domains fetched from the meta API; empty if the fetch + // failed, in which case the hardcoded list above is the baseline. + implicitEndpoints = append(implicitEndpoints, githubMetaDomains...) + for key, val := range endpoints { if isWildcardDomain(key) { wildcardEndpoints[key] = append(wildcardEndpoints[key], val...) diff --git a/agent_test.go b/agent_test.go index 1ecf212..0518218 100644 --- a/agent_test.go +++ b/agent_test.go @@ -258,7 +258,7 @@ func Test_addImplicitEndpoints_RemovesGlobalBlocklistedEndpoints(t *testing.T) { "*.example.com.": {{domainName: "*.example.com.", port: 443}}, } - filteredAllowedEndpoints, filteredWildcardEndpoints := addImplicitEndpoints(endpoints, true, globalBlocklist) + filteredAllowedEndpoints, filteredWildcardEndpoints := addImplicitEndpoints(endpoints, true, globalBlocklist, nil) if _, found := filteredAllowedEndpoints["allowed.com."]; found { t.Fatalf("expected allowed.com to be removed") @@ -277,6 +277,33 @@ func Test_addImplicitEndpoints_RemovesGlobalBlocklistedEndpoints(t *testing.T) { } } +func Test_addImplicitEndpoints_AddsGithubMetaDomains(t *testing.T) { + githubMetaDomains := []Endpoint{ + {domainName: "release-assets.github.com", port: 443}, + {domainName: "*.pkg.github.com", port: 443}, + {domainName: "blocklisted.github.io", port: 443}, + } + + globalBlocklist := NewGlobalBlocklist(&GlobalBlocklistResponse{ + Domains: []CompromisedEndpoint{{Endpoint: "blocklisted.github.io", Reason: "blocked"}}, + }) + + allowedEndpoints, wildcardEndpoints := addImplicitEndpoints(map[string][]Endpoint{}, true, globalBlocklist, githubMetaDomains) + + if _, found := allowedEndpoints["release-assets.github.com"]; !found { + t.Fatalf("expected meta domain release-assets.github.com to be added") + } + + if _, found := wildcardEndpoints["*.pkg.github.com"]; !found { + t.Fatalf("expected wildcard meta domain *.pkg.github.com to be added") + } + + // Global blocklist filtering must still apply to meta domains + if _, found := allowedEndpoints["blocklisted.github.io"]; found { + t.Fatalf("expected blocklisted meta domain to be removed") + } +} + func Test_writeDone(t *testing.T) { tests := []struct { name string diff --git a/apiclient.go b/apiclient.go index aae6bef..a6d55a1 100644 --- a/apiclient.go +++ b/apiclient.go @@ -8,6 +8,7 @@ import ( "net/http" "net/url" "path" + "strings" "time" ) @@ -193,6 +194,65 @@ func (apiclient *ApiClient) getGlobalBlocklist() (*GlobalBlocklistResponse, erro return out, nil } +type GithubMetaResponse struct { + Domains struct { + Actions []string `json:"actions"` + } `json:"domains"` +} + +// getGithubMetaDomains fetches the GitHub Actions domains from the meta API. +// Domains under githubusercontent.com are skipped since they are already +// covered by the hardcoded wildcard implicit endpoints. +func (apiclient *ApiClient) getGithubMetaDomains() ([]Endpoint, error) { + url := fmt.Sprintf("%s/github/meta", apiclient.APIURL) + + maxRetries := 3 + var lastErr error + + for i := 0; i < maxRetries; i++ { + if i > 0 { + time.Sleep(500 * time.Millisecond) + } + + resp, err := apiclient.Client.Get(url) + if err != nil { + lastErr = fmt.Errorf("[getGithubMetaDomains] error while sending request: %s", err) + continue + } + + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + lastErr = fmt.Errorf("[getGithubMetaDomains] response status not okay: status %d", resp.StatusCode) + continue + } + + body, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + lastErr = fmt.Errorf("[getGithubMetaDomains] unable to read response body: %s", err) + continue + } + + var meta GithubMetaResponse + if err := json.Unmarshal(body, &meta); err != nil { + lastErr = fmt.Errorf("[getGithubMetaDomains] unable to unmarshal response body: %s", err) + continue + } + + var endpoints []Endpoint + for _, domain := range meta.Domains.Actions { + if strings.HasSuffix(domain, "githubusercontent.com") { + continue + } + endpoints = append(endpoints, Endpoint{domainName: domain, port: 443}) + } + + return endpoints, nil + } + + return nil, fmt.Errorf("failed to fetch GitHub meta domains after %d retries: %v", maxRetries, lastErr) +} + func (apiclient *ApiClient) sendApiRequest(method, url string, body interface{}) error { jsonData, _ := json.Marshal(body) diff --git a/apiclient_test.go b/apiclient_test.go new file mode 100644 index 0000000..46df536 --- /dev/null +++ b/apiclient_test.go @@ -0,0 +1,98 @@ +package main + +import ( + "fmt" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + "time" +) + +// newTestHTTPClient uses an explicit transport so these tests keep working +// even after another test has mocked http.DefaultTransport via httpmock. +func newTestHTTPClient() *http.Client { + return &http.Client{Timeout: 3 * time.Second, Transport: &http.Transport{}} +} + +func Test_getGithubMetaDomains(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/github/meta" { + w.WriteHeader(http.StatusNotFound) + return + } + fmt.Fprint(w, `{"domains":{"actions":["github.com","api.github.com","codeload.github.com","*.actions.githubusercontent.com","objects.githubusercontent.com"]}}`) + })) + defer server.Close() + + apiclient := &ApiClient{Client: newTestHTTPClient(), APIURL: server.URL + "/v1"} + + endpoints, err := apiclient.getGithubMetaDomains() + if err != nil { + t.Fatalf("getGithubMetaDomains returned error: %v", err) + } + + want := map[string]bool{ + "github.com": false, + "api.github.com": false, + "codeload.github.com": false, + } + for _, e := range endpoints { + if e.port != 443 { + t.Fatalf("endpoint %s port = %d, want 443", e.domainName, e.port) + } + if _, ok := want[e.domainName]; !ok { + t.Fatalf("unexpected endpoint %q; githubusercontent.com domains must be filtered out", e.domainName) + } + want[e.domainName] = true + } + for domain, seen := range want { + if !seen { + t.Fatalf("expected endpoint %q missing", domain) + } + } +} + +func Test_getGithubMetaDomains_RetriesOnFailure(t *testing.T) { + var calls atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if calls.Add(1) < 3 { + w.WriteHeader(http.StatusInternalServerError) + return + } + fmt.Fprint(w, `{"domains":{"actions":["github.com"]}}`) + })) + defer server.Close() + + apiclient := &ApiClient{Client: newTestHTTPClient(), APIURL: server.URL + "/v1"} + + endpoints, err := apiclient.getGithubMetaDomains() + if err != nil { + t.Fatalf("getGithubMetaDomains returned error: %v", err) + } + if len(endpoints) != 1 || endpoints[0].domainName != "github.com" { + t.Fatalf("endpoints = %+v, want [github.com]", endpoints) + } + if got := calls.Load(); got != 3 { + t.Fatalf("server called %d times, want 3", got) + } +} + +func Test_getGithubMetaDomains_ErrorAfterRetries(t *testing.T) { + var calls atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls.Add(1) + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + apiclient := &ApiClient{Client: newTestHTTPClient(), APIURL: server.URL + "/v1"} + + endpoints, err := apiclient.getGithubMetaDomains() + if err == nil { + t.Fatalf("expected error after retries, got endpoints %+v", endpoints) + } + if got := calls.Load(); got != 3 { + t.Fatalf("server called %d times, want 3", got) + } +} From 533aa6a759a4c42e89ffd814b24db56f3215881e Mon Sep 17 00:00:00 2001 From: Rohan Prabhu Date: Sat, 22 Aug 2026 10:25:53 +0530 Subject: [PATCH 19/25] refactor: sudo test seams to per-instance fields Move runCmd and path overrides onto Sudo so the background container-cleanup goroutine never reads package state restored by tests. (cherry picked from commit bae4f1c7e4c8604b27574073fade4028b893a093) --- sudo.go | 64 ++++++++++++++++++++++++++----------- sudo_test.go | 89 +++++++++++++++++++++------------------------------- 2 files changed, 81 insertions(+), 72 deletions(-) diff --git a/sudo.go b/sudo.go index 924282b..2afcf67 100644 --- a/sudo.go +++ b/sudo.go @@ -11,21 +11,49 @@ import ( type Sudo struct { SudoersBackUpPath string -} -// sudoersFile is a var so tests can point it at a temp file -var sudoersFile = "/etc/sudoers.d/runner" + // test seams; zero values fall back to the real paths and command runner. + // These are per-instance fields rather than package vars so the background + // container-cleanup goroutine never reads state a test restores later. + sudoersFilePathOverride string + dockerSockPathOverride string + containerdSockOverride string + runCmd func(cmd string, args ...string) +} const runnerUser = "runner" -// runCmd is a var so tests can inject a fake command runner -var runCmd = run +func (s *Sudo) sudoersFilePath() string { + if s.sudoersFilePathOverride != "" { + return s.sudoersFilePathOverride + } + return "/etc/sudoers.d/runner" +} + +func (s *Sudo) dockerSockPath() string { + if s.dockerSockPathOverride != "" { + return s.dockerSockPathOverride + } + return "/var/run/docker.sock" +} + +func (s *Sudo) containerdSockPath() string { + if s.containerdSockOverride != "" { + return s.containerdSockOverride + } + return "/run/containerd/containerd.sock" +} -// socket paths are vars so tests can point them at temp files -var dockerSockPath = "/var/run/docker.sock" -var containerdSockPath = "/run/containerd/containerd.sock" +func (s *Sudo) exec(cmd string, args ...string) { + if s.runCmd != nil { + s.runCmd(cmd, args...) + return + } + run(cmd, args...) +} func (s *Sudo) disableSudo(tempDir string) error { + sudoersFile := s.sudoersFilePath() s.SudoersBackUpPath = path.Join(tempDir, "runner") err := copy(sudoersFile, s.SudoersBackUpPath) @@ -42,7 +70,7 @@ func (s *Sudo) disableSudo(tempDir string) error { func (s *Sudo) revertDisableSudo() error { if len(s.SudoersBackUpPath) > 0 { - err := copy(s.SudoersBackUpPath, sudoersFile) + err := copy(s.SudoersBackUpPath, s.sudoersFilePath()) if err != nil { return fmt.Errorf("error reverting sudoers file: %v", err) @@ -86,15 +114,15 @@ func (s *Sudo) disableSudoAndContainers(tempDir string) error { // removeSocketPermissions removes permissions from Docker and containerd sockets if they exist func (s *Sudo) removeSocketPermissions() { // Check and remove docker.sock permissions if it exists - if _, err := os.Stat(dockerSockPath); err == nil { - if err := os.Chmod(dockerSockPath, 0000); err != nil { + if _, err := os.Stat(s.dockerSockPath()); err == nil { + if err := os.Chmod(s.dockerSockPath(), 0000); err != nil { WriteLog(fmt.Sprintf("error removing docker.sock permissions: %v", err)) } } // Check and remove containerd.sock permissions if it exists - if _, err := os.Stat(containerdSockPath); err == nil { - if err := os.Chmod(containerdSockPath, 0000); err != nil { + if _, err := os.Stat(s.containerdSockPath()); err == nil { + if err := os.Chmod(s.containerdSockPath(), 0000); err != nil { WriteLog(fmt.Sprintf("error removing containerd.sock permissions: %v", err)) } } @@ -133,15 +161,15 @@ func run(cmd string, args ...string) { func (s *Sudo) uninstallDocker() error { WriteLog("Uninstalling docker") - runCmd("apt-get", "purge", "-y", + s.exec("apt-get", "purge", "-y", "docker-ce", "docker-ce-cli", "containerd.io") return nil } func (s *Sudo) removeDockerDirectoriesAndFiles() error { - runCmd("rm", "-rf", "/var/lib/docker") - runCmd("rm", "-rf", "/var/lib/containerd") - runCmd("rm", "-f", "/etc/apt/sources.list.d/docker.list") - runCmd("rm", "-f", "/etc/apt/keyrings/docker.asc") + s.exec("rm", "-rf", "/var/lib/docker") + s.exec("rm", "-rf", "/var/lib/containerd") + s.exec("rm", "-f", "/etc/apt/sources.list.d/docker.list") + s.exec("rm", "-f", "/etc/apt/keyrings/docker.asc") return nil } diff --git a/sudo_test.go b/sudo_test.go index 3cd7202..c6918c1 100644 --- a/sudo_test.go +++ b/sudo_test.go @@ -11,34 +11,29 @@ import ( const testSudoersContent = "runner ALL=(ALL) NOPASSWD:ALL\n" -func setupFakeSudoers(t *testing.T) string { +// newTestSudo returns a Sudo wired to a fake sudoers file and non-existent +// socket paths so tests never touch the machine's real sudoers or sockets. +// All seams are per-instance fields: the background container-cleanup +// goroutine may outlive the test, so it must never read package-level state +// that a test cleanup restores. +func newTestSudo(t *testing.T, runCmd func(cmd string, args ...string)) (*Sudo, string) { t.Helper() - origSudoers := sudoersFile - sudoersFile = path.Join(t.TempDir(), "runner") - t.Cleanup(func() { sudoersFile = origSudoers }) - - redirectSocketPaths(t) + dir := t.TempDir() + sudoersPath := path.Join(dir, "runner") // 0640 rather than the real file's 0440: the agent truncates as root // (which bypasses permission checks), but tests run unprivileged. - if err := os.WriteFile(sudoersFile, []byte(testSudoersContent), 0640); err != nil { + if err := os.WriteFile(sudoersPath, []byte(testSudoersContent), 0640); err != nil { t.Fatalf("failed to create fake sudoers file: %v", err) } - return sudoersFile -} - -// redirectSocketPaths points socket paths at non-existent temp files so tests -// never chmod the machine's real docker/containerd sockets. -func redirectSocketPaths(t *testing.T) { - t.Helper() - origDocker, origContainerd := dockerSockPath, containerdSockPath - dockerSockPath = path.Join(t.TempDir(), "no-such-docker.sock") - containerdSockPath = path.Join(t.TempDir(), "no-such-containerd.sock") - t.Cleanup(func() { dockerSockPath, containerdSockPath = origDocker, origContainerd }) + return &Sudo{ + sudoersFilePathOverride: sudoersPath, + dockerSockPathOverride: path.Join(dir, "no-such-docker.sock"), + containerdSockOverride: path.Join(dir, "no-such-containerd.sock"), + runCmd: runCmd, + }, sudoersPath } func Test_disableSudoAndContainers_RevokesSudoBeforeContainerCleanup(t *testing.T) { - setupFakeSudoers(t) - type observed struct { command string sudoersSize int64 @@ -49,11 +44,11 @@ func Test_disableSudoAndContainers_RevokesSudoBeforeContainerCleanup(t *testing. var mu sync.Mutex var commands []string - origRun := runCmd - runCmd = func(cmd string, args ...string) { + sudo, sudoersPath := newTestSudo(t, nil) + sudo.runCmd = func(cmd string, args ...string) { full := strings.Join(append([]string{cmd}, args...), " ") var size int64 = -1 - if fi, err := os.Stat(sudoersFile); err == nil { + if fi, err := os.Stat(sudoersPath); err == nil { size = fi.Size() } mu.Lock() @@ -65,12 +60,8 @@ func Test_disableSudoAndContainers_RevokesSudoBeforeContainerCleanup(t *testing. } <-release } - t.Cleanup(func() { - close(release) - runCmd = origRun - }) + t.Cleanup(func() { close(release) }) - sudo := &Sudo{} done := make(chan error, 1) go func() { done <- sudo.disableSudoAndContainers(t.TempDir()) @@ -87,7 +78,7 @@ func Test_disableSudoAndContainers_RevokesSudoBeforeContainerCleanup(t *testing. t.Fatal("disableSudoAndContainers blocked on container teardown; sudo revocation must complete first") } - fi, err := os.Stat(sudoersFile) + fi, err := os.Stat(sudoersPath) if err != nil { t.Fatalf("failed to stat sudoers file: %v", err) } @@ -112,16 +103,14 @@ func Test_disableSudoAndContainers_RevokesSudoBeforeContainerCleanup(t *testing. } func Test_disableSudoAndContainers_ErrorSurfacesWhenSudoersMissing(t *testing.T) { - origSudoers := sudoersFile - sudoersFile = path.Join(t.TempDir(), "does-not-exist") - t.Cleanup(func() { sudoersFile = origSudoers }) - redirectSocketPaths(t) - - origRun := runCmd - runCmd = func(cmd string, args ...string) {} - t.Cleanup(func() { runCmd = origRun }) + dir := t.TempDir() + sudo := &Sudo{ + sudoersFilePathOverride: path.Join(dir, "does-not-exist"), + dockerSockPathOverride: path.Join(dir, "no-such-docker.sock"), + containerdSockOverride: path.Join(dir, "no-such-containerd.sock"), + runCmd: func(cmd string, args ...string) {}, + } - sudo := &Sudo{} err := sudo.disableSudoAndContainers(t.TempDir()) if err == nil { t.Fatal("expected error when sudoers backup fails, got nil") @@ -133,30 +122,23 @@ func Test_disableSudoAndContainers_ErrorSurfacesWhenSudoersMissing(t *testing.T) // Socket permissions must be revoked synchronously (before return), via chmod. func Test_disableSudoAndContainers_RemovesSocketPermissions(t *testing.T) { - setupFakeSudoers(t) + sudo, _ := newTestSudo(t, func(cmd string, args ...string) {}) sockDir := t.TempDir() - origDocker, origContainerd := dockerSockPath, containerdSockPath - dockerSockPath = path.Join(sockDir, "docker.sock") - containerdSockPath = path.Join(sockDir, "containerd.sock") - t.Cleanup(func() { dockerSockPath, containerdSockPath = origDocker, origContainerd }) + sudo.dockerSockPathOverride = path.Join(sockDir, "docker.sock") + sudo.containerdSockOverride = path.Join(sockDir, "containerd.sock") - for _, p := range []string{dockerSockPath, containerdSockPath} { + for _, p := range []string{sudo.dockerSockPathOverride, sudo.containerdSockOverride} { if err := os.WriteFile(p, nil, 0660); err != nil { t.Fatalf("failed to create fake socket %s: %v", p, err) } } - origRun := runCmd - runCmd = func(cmd string, args ...string) {} - t.Cleanup(func() { runCmd = origRun }) - - sudo := &Sudo{} if err := sudo.disableSudoAndContainers(t.TempDir()); err != nil { t.Fatalf("disableSudoAndContainers returned error: %v", err) } - for _, p := range []string{dockerSockPath, containerdSockPath} { + for _, p := range []string{sudo.dockerSockPathOverride, sudo.containerdSockOverride} { fi, err := os.Stat(p) if err != nil { t.Fatalf("failed to stat %s: %v", p, err) @@ -168,9 +150,8 @@ func Test_disableSudoAndContainers_RemovesSocketPermissions(t *testing.T) { } func Test_disableSudo_BackupAndRevert(t *testing.T) { - setupFakeSudoers(t) + sudo, sudoersPath := newTestSudo(t, func(cmd string, args ...string) {}) - sudo := &Sudo{} tempDir := t.TempDir() if err := sudo.disableSudo(tempDir); err != nil { t.Fatalf("disableSudo returned error: %v", err) @@ -184,7 +165,7 @@ func Test_disableSudo_BackupAndRevert(t *testing.T) { t.Fatalf("backup content = %q, want %q", string(backup), testSudoersContent) } - fi, err := os.Stat(sudoersFile) + fi, err := os.Stat(sudoersPath) if err != nil { t.Fatalf("failed to stat sudoers file: %v", err) } @@ -195,7 +176,7 @@ func Test_disableSudo_BackupAndRevert(t *testing.T) { if err := sudo.revertDisableSudo(); err != nil { t.Fatalf("revertDisableSudo returned error: %v", err) } - restored, err := os.ReadFile(sudoersFile) + restored, err := os.ReadFile(sudoersPath) if err != nil { t.Fatalf("failed to read restored sudoers file: %v", err) } From 0713182aff7b752ab0c5287a82ba2fb6e623391d Mon Sep 17 00:00:00 2001 From: Rohan Prabhu Date: Mon, 24 Aug 2026 13:14:14 +0530 Subject: [PATCH 20/25] fix: convert GitHub meta domains to Fqdn form (cherry picked from commit 4ca65f7a225b946084e29ccc0125b18111eec645) --- agent_test.go | 18 ++++++++++-------- apiclient.go | 6 +++++- apiclient_test.go | 18 +++++++++++------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/agent_test.go b/agent_test.go index 0518218..49b779d 100644 --- a/agent_test.go +++ b/agent_test.go @@ -278,10 +278,12 @@ func Test_addImplicitEndpoints_RemovesGlobalBlocklistedEndpoints(t *testing.T) { } func Test_addImplicitEndpoints_AddsGithubMetaDomains(t *testing.T) { + // Meta domains arrive in Fqdn form (trailing dot), as produced by + // getGithubMetaDomains. githubMetaDomains := []Endpoint{ - {domainName: "release-assets.github.com", port: 443}, - {domainName: "*.pkg.github.com", port: 443}, - {domainName: "blocklisted.github.io", port: 443}, + {domainName: "ghcr.io.", port: 443}, + {domainName: "*.pkg.github.com.", port: 443}, + {domainName: "blocklisted.github.io.", port: 443}, } globalBlocklist := NewGlobalBlocklist(&GlobalBlocklistResponse{ @@ -290,16 +292,16 @@ func Test_addImplicitEndpoints_AddsGithubMetaDomains(t *testing.T) { allowedEndpoints, wildcardEndpoints := addImplicitEndpoints(map[string][]Endpoint{}, true, globalBlocklist, githubMetaDomains) - if _, found := allowedEndpoints["release-assets.github.com"]; !found { - t.Fatalf("expected meta domain release-assets.github.com to be added") + if _, found := allowedEndpoints["ghcr.io."]; !found { + t.Fatalf("expected meta domain ghcr.io. to be added") } - if _, found := wildcardEndpoints["*.pkg.github.com"]; !found { - t.Fatalf("expected wildcard meta domain *.pkg.github.com to be added") + if _, found := wildcardEndpoints["*.pkg.github.com."]; !found { + t.Fatalf("expected wildcard meta domain *.pkg.github.com. to be added") } // Global blocklist filtering must still apply to meta domains - if _, found := allowedEndpoints["blocklisted.github.io"]; found { + if _, found := allowedEndpoints["blocklisted.github.io."]; found { t.Fatalf("expected blocklisted meta domain to be removed") } } diff --git a/apiclient.go b/apiclient.go index a6d55a1..465f009 100644 --- a/apiclient.go +++ b/apiclient.go @@ -10,6 +10,8 @@ import ( "path" "strings" "time" + + "github.com/miekg/dns" ) type DNSRecord struct { @@ -244,7 +246,9 @@ func (apiclient *ApiClient) getGithubMetaDomains() ([]Endpoint, error) { if strings.HasSuffix(domain, "githubusercontent.com") { continue } - endpoints = append(endpoints, Endpoint{domainName: domain, port: 443}) + // Fqdn form (trailing dot) is required for wildcard matching: + // matchWildcardDomain compares raw suffixes against Fqdn queries. + endpoints = append(endpoints, Endpoint{domainName: dns.Fqdn(domain), port: 443}) } return endpoints, nil diff --git a/apiclient_test.go b/apiclient_test.go index 46df536..acd43a5 100644 --- a/apiclient_test.go +++ b/apiclient_test.go @@ -21,7 +21,7 @@ func Test_getGithubMetaDomains(t *testing.T) { w.WriteHeader(http.StatusNotFound) return } - fmt.Fprint(w, `{"domains":{"actions":["github.com","api.github.com","codeload.github.com","*.actions.githubusercontent.com","objects.githubusercontent.com"]}}`) + fmt.Fprint(w, `{"domains":{"actions":["github.com","api.github.com","codeload.github.com","*.github.com","*.actions.githubusercontent.com","objects.githubusercontent.com"]}}`) })) defer server.Close() @@ -32,17 +32,21 @@ func Test_getGithubMetaDomains(t *testing.T) { t.Fatalf("getGithubMetaDomains returned error: %v", err) } + // Domains must come back in Fqdn form (trailing dot): wildcard matching + // in the DNS proxy compares raw suffixes against Fqdn query names, so a + // dotless wildcard like "*.github.com" would never match. want := map[string]bool{ - "github.com": false, - "api.github.com": false, - "codeload.github.com": false, + "github.com.": false, + "api.github.com.": false, + "codeload.github.com.": false, + "*.github.com.": false, } for _, e := range endpoints { if e.port != 443 { t.Fatalf("endpoint %s port = %d, want 443", e.domainName, e.port) } if _, ok := want[e.domainName]; !ok { - t.Fatalf("unexpected endpoint %q; githubusercontent.com domains must be filtered out", e.domainName) + t.Fatalf("unexpected endpoint %q; want Fqdn form, githubusercontent.com domains filtered out", e.domainName) } want[e.domainName] = true } @@ -70,8 +74,8 @@ func Test_getGithubMetaDomains_RetriesOnFailure(t *testing.T) { if err != nil { t.Fatalf("getGithubMetaDomains returned error: %v", err) } - if len(endpoints) != 1 || endpoints[0].domainName != "github.com" { - t.Fatalf("endpoints = %+v, want [github.com]", endpoints) + if len(endpoints) != 1 || endpoints[0].domainName != "github.com." { + t.Fatalf("endpoints = %+v, want [github.com.]", endpoints) } if got := calls.Load(); got != 3 { t.Fatalf("server called %d times, want 3", got) From f4b66510c579b0797b500849673ce3b229d3f6af Mon Sep 17 00:00:00 2001 From: Jatin <84621253+h0x0er@users.noreply.github.com> Date: Mon, 7 Sep 2026 12:09:35 +0530 Subject: [PATCH 21/25] changes for building arm64 binary --- .goreleaser.yml | 3 ++- procmon_linux.go | 9 ++++++++- releasers/int.yml | 3 ++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index c6e41fc..5b0a223 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -25,4 +25,5 @@ builds: # Format is `{goos}_{goarch}` with optionally a suffix with `_{goarm}` or `_{gomips}`. # This overrides `goos`, `goarch`, `goarm`, `gomips` and `ignores`. targets: - - linux_amd64 \ No newline at end of file + - linux_amd64 + - linux_arm64 diff --git a/procmon_linux.go b/procmon_linux.go index 9f87550..1cd711c 100644 --- a/procmon_linux.go +++ b/procmon_linux.go @@ -5,6 +5,7 @@ package main import ( "fmt" + "runtime" "io/ioutil" "os" @@ -55,7 +56,13 @@ func (p *ProcessMonitor) MonitorProcesses(errc chan error) { if len(workingDirectory) == 0 { workingDirectory = "/home/runner" } - r, _ := flags.Parse(fmt.Sprintf("-a exit,always -F dir=%s -F perm=wa -S open -S openat -S rename -S renameat -k %s", workingDirectory, fileMonitorTag)) + + auditRule := fmt.Sprintf("-a exit,always -F dir=%s -F perm=wa -S open -S openat -S rename -S renameat -k %s", workingDirectory, fileMonitorTag) + if runtime.GOARCH == "arm64" { + auditRule = fmt.Sprintf("-a exit,always -F dir=%s -F perm=wa -S openat -S renameat -k %s", workingDirectory, fileMonitorTag) + } + + r, _ := flags.Parse(auditRule) actualBytes, _ := rule.Build(r) diff --git a/releasers/int.yml b/releasers/int.yml index 125acde..97ec794 100644 --- a/releasers/int.yml +++ b/releasers/int.yml @@ -25,4 +25,5 @@ builds: # Format is `{goos}_{goarch}` with optionally a suffix with `_{goarm}` or `_{gomips}`. # This overrides `goos`, `goarch`, `goarm`, `gomips` and `ignores`. targets: - - linux_amd64 \ No newline at end of file + - linux_amd64 + - linux_arm64 \ No newline at end of file From 400f03f31c6b5ac071de91a61fa8e9b000c1edf5 Mon Sep 17 00:00:00 2001 From: Jatin <84621253+h0x0er@users.noreply.github.com> Date: Mon, 28 Sep 2026 13:06:35 +0530 Subject: [PATCH 22/25] refactored rule creation --- procmon_linux.go | 13 +++++++++---- procmon_linux_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/procmon_linux.go b/procmon_linux.go index 1cd711c..4730ce1 100644 --- a/procmon_linux.go +++ b/procmon_linux.go @@ -57,10 +57,7 @@ func (p *ProcessMonitor) MonitorProcesses(errc chan error) { workingDirectory = "/home/runner" } - auditRule := fmt.Sprintf("-a exit,always -F dir=%s -F perm=wa -S open -S openat -S rename -S renameat -k %s", workingDirectory, fileMonitorTag) - if runtime.GOARCH == "arm64" { - auditRule = fmt.Sprintf("-a exit,always -F dir=%s -F perm=wa -S openat -S renameat -k %s", workingDirectory, fileMonitorTag) - } + auditRule := fileMonitorAuditRule(workingDirectory, runtime.GOARCH) r, _ := flags.Parse(auditRule) @@ -157,6 +154,14 @@ func (p *ProcessMonitor) MonitorProcesses(errc chan error) { p.receive(client) } +func fileMonitorAuditRule(workingDirectory, goarch string) string { + if goarch == "arm64" { + return fmt.Sprintf("-a exit,always -F dir=%s -F perm=wa -S openat -S renameat -k %s", workingDirectory, fileMonitorTag) + } + + return fmt.Sprintf("-a exit,always -F dir=%s -F perm=wa -S open -S openat -S rename -S renameat -k %s", workingDirectory, fileMonitorTag) +} + func (p *ProcessMonitor) receive(r *libaudit.AuditClient) error { p.Events = make(map[int]*Event) diff --git a/procmon_linux_test.go b/procmon_linux_test.go index 9b7753c..a6189fe 100644 --- a/procmon_linux_test.go +++ b/procmon_linux_test.go @@ -8,6 +8,36 @@ import ( "testing" ) +func Test_fileMonitorAuditRule(t *testing.T) { + tests := []struct { + name string + workingDirectory string + goarch string + want string + }{ + { + name: "arm64 excludes unsupported syscalls", + workingDirectory: "/home/runner/work/repo", + goarch: "arm64", + want: "-a exit,always -F dir=/home/runner/work/repo -F perm=wa -S openat -S renameat -k filemon", + }, + { + name: "default includes open and rename syscalls", + workingDirectory: "/home/runner/work/repo", + goarch: "amd64", + want: "-a exit,always -F dir=/home/runner/work/repo -F perm=wa -S open -S openat -S rename -S renameat -k filemon", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := fileMonitorAuditRule(tt.workingDirectory, tt.goarch); got != tt.want { + t.Errorf("fileMonitorAuditRule() = %q, want %q", got, tt.want) + } + }) + } +} + func Test_getProcessExe(t *testing.T) { type args struct { pid string From 30ccca72613f1a9768f7b5c6860e9d16d130c731 Mon Sep 17 00:00:00 2001 From: prateek-stepsecurity Date: Wed, 5 Aug 2026 19:25:48 +0530 Subject: [PATCH 23/25] fix: set the auditd event status as blocked for global block ip currenlty we do not get global block ip event from nflog --- eventhandler.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/eventhandler.go b/eventhandler.go index 1051338..3a8cbfd 100644 --- a/eventhandler.go +++ b/eventhandler.go @@ -198,13 +198,23 @@ func (eventHandler *EventHandler) handleNetworkEvent(event *Event) { } reverseLookUp := eventHandler.DNSProxy.GetReverseIPLookup(event.IPAddress) - eventHandler.ApiClient.sendNetConnection(eventHandler.CorrelationId, eventHandler.Repo, event.IPAddress, event.Port, reverseLookUp, "", "", "", event.Timestamp, tool) + + status := "" + matchedPolicy := "" + reason := "" + if eventHandler.DNSProxy.GlobalBlocklist != nil && eventHandler.DNSProxy.GlobalBlocklist.IsIPAddressBlocked(event.IPAddress) { + status = "Dropped" + matchedPolicy = GlobalBlocklistMatchedPolicy + reason = eventHandler.DNSProxy.GlobalBlocklist.BlockedIPAddressReason(event.IPAddress) + } + + eventHandler.ApiClient.sendNetConnection(eventHandler.CorrelationId, eventHandler.Repo, event.IPAddress, event.Port, reverseLookUp, status, matchedPolicy, reason, event.Timestamp, tool) process := "" if image == "" { process = tool.Name } - WriteLog(fmt.Sprintf("endpoint called ip address:port %s:%s, domain: %s, pid: %s, process: %s", event.IPAddress, event.Port, reverseLookUp, event.Pid, process)) + WriteLog(fmt.Sprintf("endpoint called ip address:port %s:%s, domain: %s, pid: %s, process: %s, status: %s", event.IPAddress, event.Port, reverseLookUp, event.Pid, process, status)) } } From f5a5f43670e536a3a1ca2a3496e680a7d6bd1f20 Mon Sep 17 00:00:00 2001 From: Jatin <84621253+h0x0er@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:07:38 +0530 Subject: [PATCH 24/25] Merge pull request #481 from Prateek-stepsecurity/pn/int/fix fix: send domain name to custom detction rule manager for processing ntw rule --- dnsproxy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsproxy.go b/dnsproxy.go index 454dfeb..d162698 100644 --- a/dnsproxy.go +++ b/dnsproxy.go @@ -251,7 +251,7 @@ func (proxy *DNSProxy) getIPByDomain(domain string) (string, error) { go proxy.ApiClient.sendDNSRecord(proxy.CorrelationId, proxy.Repo, domain, answer.Data, "", "") - go proxy.submitDNSEvent(answer.Data) + go proxy.submitDNSEvent(domain) return answer.Data, nil From 17023a44dd18e2222c20ae1026028f85084b9dfc Mon Sep 17 00:00:00 2001 From: Jatin <84621253+h0x0er@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:32:01 +0530 Subject: [PATCH 25/25] Jatin/revert GitHub dns exp (#498) * revert github dns experiment when global feature flag is enabled * also detect and remove github dns experiment drop-in under /run * chore: bump go to 1.27.3 * revert github dns experiment in fail-safe order and replace resolv.conf symlink atomically --- .github/workflows/int.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- agent.go | 4 ++ github_dns_experiment.go | 112 ++++++++++++++++++++++++++++++++++ global_feature_flags.go | 1 + go.mod | 2 +- release-monitor.yml | 2 +- 8 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 github_dns_experiment.go diff --git a/.github/workflows/int.yml b/.github/workflows/int.yml index c33cf43..a700d8a 100644 --- a/.github/workflows/int.yml +++ b/.github/workflows/int.yml @@ -23,7 +23,7 @@ jobs: - name: Set up Go uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4 with: - go-version: 1.26.2 + go-version: 1.27.3 - name: Configure .netrc run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 902ab60..0df8365 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: - name: Set up Go uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4 with: - go-version: 1.26.2 + go-version: 1.27.3 - name: Configure .netrc run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6dd66b5..22b99bb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Go uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4 with: - go-version: 1.26.2 + go-version: 1.27.3 - name: Configure .netrc run: | diff --git a/agent.go b/agent.go index 152417a..9ae1ef9 100644 --- a/agent.go +++ b/agent.go @@ -82,6 +82,10 @@ func Run(ctx context.Context, configFilePath string, hostDNSServer DNSServer, WriteLog("\n") globalFlags := GetGlobalFeatureFlags() + if globalFlags.RevertGithubDNSExperiment { + RevertGithubDNSExperiment() + } + globalBlocklistResponse, err := apiclient.getGlobalBlocklist() globalBlocklist := NewGlobalBlocklist(globalBlocklistResponse) if err != nil { diff --git a/github_dns_experiment.go b/github_dns_experiment.go new file mode 100644 index 0000000..fe755a1 --- /dev/null +++ b/github_dns_experiment.go @@ -0,0 +1,112 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +// GitHub is experimenting with a DNS resolver and an outbound proxy on hosted runners, +// which bypasses the agent's DNS server and results in allowed connections being dropped. +// ref: https://github.com/step-security/harden-runner/issues/708 +const ( + githubDNSResolverAddress = "127.0.0.100" + etcResolvConfPath = "/etc/resolv.conf" + stubResolvConfPath = "/run/systemd/resolve/stub-resolv.conf" +) + +var githubDNSExperimentServices = []string{"egress-dns-resolver.service", "egress-outbound-proxy.service"} + +// systemd-resolved reads drop-ins from both /etc and /run +var githubDNSDropInPaths = []string{"/etc/systemd/resolved.conf.d/90-egress-proxy.conf", "/run/systemd/resolved.conf.d/90-egress-proxy.conf"} + +// RevertGithubDNSExperiment reverts github's dns experiment, if it is present on the runner +func RevertGithubDNSExperiment() { + if !isGithubDNSExperimentPresent() { + WriteLog("[github-dns-experiment] not detected") + return + } + + WriteLog("[github-dns-experiment] detected, reverting") + if err := revertGithubDNSExperiment(); err != nil { + WriteLog(fmt.Sprintf("[github-dns-experiment] error reverting: %v", err)) + return + } + WriteLog("[github-dns-experiment] reverted") +} + +func isGithubDNSExperimentPresent() bool { + for _, dropInPath := range githubDNSDropInPaths { + if _, err := os.Stat(dropInPath); err == nil { + return true + } + } + + for _, service := range githubDNSExperimentServices { + if isServiceActive(service) { + return true + } + } + + resolvConf, err := os.ReadFile(etcResolvConfPath) + return err == nil && strings.Contains(string(resolvConf), githubDNSResolverAddress) +} + +func revertGithubDNSExperiment() error { + // repoint dns away from github's resolver before stopping it, so a failure at any step leaves dns working + for _, dropInPath := range githubDNSDropInPaths { + if err := os.Remove(dropInPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("error removing %s: %v", dropInPath, err) + } + } + + if err := exec.Command("sudo", "systemctl", "restart", "systemd-resolved").Run(); err != nil { + return fmt.Errorf("error restarting systemd-resolved: %v", err) + } + + if err := linkResolvConfToStub(); err != nil { + return err + } + + // github's services are no longer in the dns path, so failing to stop them is not fatal + for _, service := range githubDNSExperimentServices { + if !isServiceActive(service) { + continue + } + if err := exec.Command("sudo", "systemctl", "stop", service).Run(); err != nil { + WriteLog(fmt.Sprintf("[github-dns-experiment] error stopping %s: %v", service, err)) + } + } + + if err := exec.Command("sudo", "resolvectl", "flush-caches").Run(); err != nil { + WriteLog(fmt.Sprintf("[github-dns-experiment] error flushing dns caches: %v", err)) + } + + return nil +} + +// linkResolvConfToStub atomically points /etc/resolv.conf to systemd-resolved's stub resolver +func linkResolvConfToStub() error { + if _, err := os.Stat(stubResolvConfPath); err != nil { + return fmt.Errorf("error finding %s: %v", stubResolvConfPath, err) + } + + tmpPath := etcResolvConfPath + ".tmp" + os.Remove(tmpPath) + if err := os.Symlink(stubResolvConfPath, tmpPath); err != nil { + return fmt.Errorf("error creating %s: %v", tmpPath, err) + } + + if err := os.Rename(tmpPath, etcResolvConfPath); err != nil { + os.Remove(tmpPath) + return fmt.Errorf("error linking %s to %s: %v", etcResolvConfPath, stubResolvConfPath, err) + } + + return nil +} + +func isServiceActive(service string) bool { + output, _ := exec.Command("systemctl", "is-active", service).Output() + return strings.TrimSpace(string(output)) == "active" +} diff --git a/global_feature_flags.go b/global_feature_flags.go index 81201aa..eca7ce5 100644 --- a/global_feature_flags.go +++ b/global_feature_flags.go @@ -15,6 +15,7 @@ type GlobalFeatureFlags struct { EnableArmour bool `json:"enable_armour"` EnableCustomDetectionRules bool `json:"enable_custom_detection_rules"` DisableKillEnforcement bool `json:"disable_kill_enforcement,omitempty"` + RevertGithubDNSExperiment bool `json:"revert_github_dns_experiment,omitempty"` } // GlobalFeatureFlagManager manages fetching and caching of global feature flags. diff --git a/go.mod b/go.mod index 267f570..613714b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/step-security/agent -go 1.26.2 +go 1.27.3 require ( github.com/coreos/go-iptables v0.6.0 diff --git a/release-monitor.yml b/release-monitor.yml index dbaaf08..84d4614 100644 --- a/release-monitor.yml +++ b/release-monitor.yml @@ -7,7 +7,7 @@ release-process: - artifact: agent_{{.Version}}_linux_amd64.tar.gz binary: agent build-command: go build -trimpath -ldflags="-s -w -X main.version={{.Version}} -X main.commit={{.FullCommit}} -X main.date=123 -X main.ReleaseTag={{.Tag}}" - go-version: 1.19.8 + go-version: 1.27.3 pipeline: github-action: repo: step-security/agent