From 960b278f0079791b503511c9901fe7a276adca28 Mon Sep 17 00:00:00 2001
From: IdanK
Date: Sat, 1 Aug 2026 15:29:36 +0300
Subject: [PATCH 1/4] Update manifest
---
cmd/main.go | 4 ++++
internal/module/manager.go | 2 +-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/cmd/main.go b/cmd/main.go
index 352cc9c..256b088 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -6,6 +6,7 @@ import (
"errors"
"flag"
"fmt"
+ "goscouter/internal/management"
"io"
"os"
"os/signal"
@@ -52,6 +53,9 @@ func main() {
func run(target string) error {
printBanner()
+ if err := management.InstallModule(context.Background(), "https://github.com/GoScouter/nginx-module"); err != nil {
+ panic(err)
+ }
if err := versions.SuggestUpdate(VERSION); err != nil {
return fmt.Errorf("update check: %w", err)
diff --git a/internal/module/manager.go b/internal/module/manager.go
index 98c1358..a410bd4 100644
--- a/internal/module/manager.go
+++ b/internal/module/manager.go
@@ -127,7 +127,7 @@ func ExternalDir() (string, error) {
if err != nil {
return "", err
}
- return filepath.Join(cacheDir, "gs"), nil
+ return filepath.Join(cacheDir, "gs", "modules"), nil
}
func (m *Manager) LoadExternals(ctx context.Context) error {
From b5cbcfb99d431bcd8faa173be80bdbb21af4c4ab Mon Sep 17 00:00:00 2001
From: IdanK
Date: Sat, 1 Aug 2026 16:37:46 +0300
Subject: [PATCH 2/4] Implement install & uninstall methods
---
cmd/main.go | 28 +++-
go.mod | 20 +++
go.sum | 101 +++++++++++++-
internal/management/install.go | 220 +++++++++++++++++++++++++++++++
internal/management/uninstall.go | 70 ++++++++++
internal/module/graph.go | 4 +
pkg/modules/manifest.go | 16 +++
7 files changed, 457 insertions(+), 2 deletions(-)
create mode 100644 internal/management/install.go
create mode 100644 internal/management/uninstall.go
create mode 100644 pkg/modules/manifest.go
diff --git a/cmd/main.go b/cmd/main.go
index 256b088..dbf2b7c 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -33,10 +33,36 @@ var interrupted atomic.Bool
func main() {
version := flag.Bool("version", false, "Returns goscouter cli version")
targetSite := flag.String("target", "", "The site to target")
+
+ install := flag.String("install", "", "Module to install")
+ uninstall := flag.String("uninstall", "", "Module to uninstall (author:name)")
+
flag.Parse()
+ if *install != "" {
+ if err := management.InstallModule(context.Background(), *install); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to install modules %s. %v\n", *install, err)
+ os.Exit(1)
+ return
+ }
+
+ os.Exit(0)
+ return
+ }
+
+ if *uninstall != "" {
+ if err := management.UninstallModule(context.Background(), *uninstall); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to uninstall module %s. %v\n", *uninstall, err)
+ os.Exit(1)
+ return
+ }
+
+ os.Exit(0)
+ return
+ }
+
if *version {
- fmt.Println(versionString())
+ fmt.Println("Version: " + versionString())
return
}
diff --git a/go.mod b/go.mod
index 133b153..1d172f7 100644
--- a/go.mod
+++ b/go.mod
@@ -4,13 +4,33 @@ go 1.26.4
require (
github.com/GoScouter/sdk v1.0.9
+ github.com/go-git/go-git/v5 v5.19.2
github.com/google/go-github v17.0.0+incompatible
github.com/stevenle/topsort v0.2.0
golang.org/x/term v0.45.0
)
require (
+ dario.cat/mergo v1.0.0 // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
+ github.com/ProtonMail/go-crypto v1.1.6 // indirect
+ github.com/cloudflare/circl v1.6.3 // indirect
+ github.com/cyphar/filepath-securejoin v0.6.1 // indirect
+ github.com/emirpasic/gods v1.18.1 // indirect
+ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
+ github.com/go-git/go-billy/v5 v5.9.0 // indirect
+ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
+ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
+ github.com/kevinburke/ssh_config v1.2.0 // indirect
+ github.com/klauspost/cpuid/v2 v2.3.0 // indirect
+ github.com/pjbgf/sha1cd v0.6.0 // indirect
+ github.com/sergi/go-diff v1.4.0 // indirect
+ github.com/skeema/knownhosts v1.3.1 // indirect
+ github.com/xanzy/ssh-agent v0.3.3 // indirect
+ golang.org/x/crypto v0.54.0 // indirect
+ golang.org/x/net v0.56.0 // indirect
golang.org/x/sys v0.47.0 // indirect
+ gopkg.in/warnings.v0 v0.1.2 // indirect
)
diff --git a/go.sum b/go.sum
index 445435f..baed647 100644
--- a/go.sum
+++ b/go.sum
@@ -1,16 +1,115 @@
+dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
+dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/GoScouter/sdk v1.0.9 h1:3Dy2l6/oMkdD01TtW2ZOgg7S7iOQFXW2bEIg2AoToZM=
github.com/GoScouter/sdk v1.0.9/go.mod h1:MvG1fWNCbXBz7rXjZA2JMD0KyNOiWJJFrb9ti3b9H8U=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
+github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
+github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
+github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
+github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
+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/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
+github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
+github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
+github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
+github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
+github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
+github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
+github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
+github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
+github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
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/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
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/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
+github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
+github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+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/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
+github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
+github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
+github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
+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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+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.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
+github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/stevenle/topsort v0.2.0 h1:LLWgtp34HPX6/RBDRS0kElVxGOTzGBLI1lSAa5Lb46k=
github.com/stevenle/topsort v0.2.0/go.mod h1:ck2WG2/ZrOr6dLApQ/5Xrqy5wv3T0qhKYWE7r9tkibc=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+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/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
+github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
+golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
+golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
+golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
+golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
+golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/management/install.go b/internal/management/install.go
new file mode 100644
index 0000000..8f77837
--- /dev/null
+++ b/internal/management/install.go
@@ -0,0 +1,220 @@
+package management
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "goscouter/internal/module"
+ "goscouter/internal/style"
+ "goscouter/internal/utils"
+ "goscouter/pkg/modules"
+ "io"
+ "net/http"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
+ "runtime"
+ "strings"
+ "time"
+
+ "github.com/GoScouter/sdk"
+ "github.com/go-git/go-git/v5"
+)
+
+var modulePattern = regexp.MustCompile(`^(https?://[^@]+)(?:@([^@]+))?$`)
+
+const infoTimeout = 5 * time.Second
+
+func InstallModule(ctx context.Context, input string) error {
+ matches := modulePattern.FindStringSubmatch(input)
+ if matches == nil {
+ return fmt.Errorf("invalid module")
+ }
+
+ url := matches[1]
+ version := matches[2]
+
+ dir, err := os.MkdirTemp("/tmp", "module-*")
+ if err != nil {
+ return err
+ }
+ defer os.RemoveAll(dir)
+
+ step("Fetching manifest from %s", style.Bold(url))
+ _, err = git.PlainClone(dir, false, &git.CloneOptions{
+ URL: url,
+ })
+ if err != nil {
+ return err
+ }
+
+ data, err := os.ReadFile(filepath.Join(dir, "manifest.json"))
+ if err != nil {
+ return err
+ }
+
+ var manifest modules.Manifest
+ err = json.Unmarshal(data, &manifest)
+ if err != nil {
+ return err
+ }
+
+ if version == "latest" || version == "" {
+ version = manifest.Latest
+ }
+
+ vers, ok := manifest.Versions[version]
+ if !ok {
+ return fmt.Errorf("cannot find matching version (%s)", version)
+ }
+
+ platform := runtime.GOOS + "-" + runtime.GOARCH
+ artifact, ok := vers.Artifacts[platform]
+ if !ok {
+ return fmt.Errorf("cannot find matching artifact for your architecture")
+ }
+
+ if artifact.Checksum == "" {
+ return fmt.Errorf("manifest lists no checksum for %s", platform)
+ }
+
+ step("Resolved %s %s for %s", style.Bold(manifest.Name), style.Bold(version), platform)
+
+ modulesDir, err := module.ExternalDir()
+ if err != nil {
+ return err
+ }
+ if err := os.MkdirAll(modulesDir, 0o755); err != nil {
+ return err
+ }
+
+ filename := path.Base(artifact.Url)
+ filePath := filepath.Join(modulesDir, filename)
+
+ if installed, err := utils.CalculateChecksum(filePath); err == nil {
+ if strings.EqualFold(installed, artifact.Checksum) {
+ return fmt.Errorf("%s %s is already installed at %s", manifest.Name, version, filePath)
+ }
+ } else if !os.IsNotExist(err) {
+ return err
+ }
+
+ step("Downloading %s", style.Dim(artifact.Url))
+ resp, err := http.Get(artifact.Url)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("no binary was found (HTTP %d)", resp.StatusCode)
+ }
+
+ staged := filepath.Join(modulesDir, fmt.Sprintf(".%s.download.%d", filename, os.Getpid()))
+ out, err := os.OpenFile(staged, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
+ if err != nil {
+ return err
+ }
+
+ hash := sha256.New()
+ if _, err := io.Copy(io.MultiWriter(out, hash), resp.Body); err != nil {
+ out.Close()
+ os.Remove(staged)
+ return err
+ }
+ if err := out.Close(); err != nil {
+ os.Remove(staged)
+ return err
+ }
+
+ got := hex.EncodeToString(hash.Sum(nil))
+ if !strings.EqualFold(got, artifact.Checksum) {
+ os.Remove(staged)
+ return fmt.Errorf(
+ "checksum mismatch for %s\n expected: %s\n actual: %s\nThe download may be corrupt or tampered with — not installing",
+ filename, artifact.Checksum, got,
+ )
+ }
+
+ step("Checksum verified %s", style.Dim(got))
+
+ manager := module.NewManager()
+ if err := manager.LoadExternals(ctx); err != nil {
+ os.Remove(staged)
+ return err
+ }
+
+ if err := os.Chmod(staged, 0o755); err != nil {
+ os.Remove(staged)
+ return err
+ }
+
+ info, err := moduleInfo(ctx, staged)
+ if err != nil {
+ os.Remove(staged)
+ return err
+ }
+
+ key := module.Key(module.Namespace(info))
+ step("Module identified as %s", style.Bold(key))
+
+ if manager.GetInternal(key) != nil {
+ os.Remove(staged)
+ return fmt.Errorf("%s is a built-in module", key)
+ }
+
+ var outdated string
+ if other := manager.GetExternal(key); other != nil && other.Path != filePath {
+ if strings.EqualFold(other.Checksum, artifact.Checksum) {
+ os.Remove(staged)
+ return fmt.Errorf("%s %s is already installed at %s", key, version, other.Path)
+ }
+ outdated = other.Path
+ notice("Replacing older install at %s", style.Dim(other.Path))
+ }
+
+ if err := os.Rename(staged, filePath); err != nil {
+ os.Remove(staged)
+ return err
+ }
+
+ if outdated != "" {
+ os.Remove(outdated)
+ }
+
+ step("Installed %s %s to %s", style.Bold(key), style.Bold(version), style.Dim(filePath))
+
+ return nil
+}
+
+func step(format string, a ...any) {
+ fmt.Printf("%s\r\n", style.Foundf(format, a...))
+}
+
+func notice(format string, a ...any) {
+ fmt.Printf("%s\r\n", style.Alertf(format, a...))
+}
+
+func moduleInfo(ctx context.Context, path string) (sdk.ModuleInfo, error) {
+ bin, err := sdk.Open(path)
+ if err != nil {
+ return sdk.ModuleInfo{}, err
+ }
+ defer bin.Close()
+
+ ctx, cancel := context.WithTimeout(ctx, infoTimeout)
+ defer cancel()
+
+ info, err := bin.Info(ctx)
+ if err != nil {
+ return sdk.ModuleInfo{}, fmt.Errorf("reading info: %w", err)
+ }
+ if !info.IsValid() {
+ return sdk.ModuleInfo{}, fmt.Errorf("%s reports invalid info", filepath.Base(path))
+ }
+
+ return info, nil
+}
diff --git a/internal/management/uninstall.go b/internal/management/uninstall.go
new file mode 100644
index 0000000..8132406
--- /dev/null
+++ b/internal/management/uninstall.go
@@ -0,0 +1,70 @@
+package management
+
+import (
+ "context"
+ "fmt"
+ "goscouter/internal/module"
+ "goscouter/internal/style"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/GoScouter/sdk"
+)
+
+var namePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`)
+
+func UninstallModule(ctx context.Context, input string) error {
+ author, name, ok := strings.Cut(strings.TrimSpace(input), ":")
+ if !ok || !namePattern.MatchString(author) || !namePattern.MatchString(name) {
+ return fmt.Errorf("invalid module, expected author:name (got %q)", input)
+ }
+
+ key := module.Key(sdk.ModuleNamespace{Author: author, Name: name})
+
+ if module.IsInternalAuthor(author) {
+ return fmt.Errorf("%s is a built-in module and cannot be uninstalled", key)
+ }
+
+ modulesDir, err := module.ExternalDir()
+ if err != nil {
+ return err
+ }
+
+ step("Looking for %s in %s", style.Bold(key), style.Dim(modulesDir))
+
+ manager := module.NewManager()
+ if err := manager.LoadExternals(ctx); err != nil {
+ return err
+ }
+
+ if manager.GetInternal(key) != nil {
+ return fmt.Errorf("%s is a built-in module and cannot be uninstalled", key)
+ }
+
+ external := manager.GetExternal(key)
+ if external == nil {
+ return fmt.Errorf("%s is not installed", key)
+ }
+
+ target, err := filepath.EvalSymlinks(external.Path)
+ if err != nil {
+ return err
+ }
+ root, err := filepath.EvalSymlinks(modulesDir)
+ if err != nil {
+ return err
+ }
+ if filepath.Dir(target) != root {
+ return fmt.Errorf("refusing to remove %s: it lives outside %s", target, root)
+ }
+
+ if err := os.Remove(target); err != nil {
+ return err
+ }
+
+ step("Uninstalled %s from %s", style.Bold(key), style.Dim(target))
+
+ return nil
+}
diff --git a/internal/module/graph.go b/internal/module/graph.go
index 25c85a5..b688c6c 100644
--- a/internal/module/graph.go
+++ b/internal/module/graph.go
@@ -16,6 +16,10 @@ func Namespace(info sdk.ModuleInfo) sdk.ModuleNamespace {
return sdk.ModuleNamespace{Name: info.Name, Author: info.Author}
}
+func IsInternalAuthor(author string) bool {
+ return author == internalAuthor
+}
+
const everything = "*"
type Graph struct {
diff --git a/pkg/modules/manifest.go b/pkg/modules/manifest.go
new file mode 100644
index 0000000..6edc785
--- /dev/null
+++ b/pkg/modules/manifest.go
@@ -0,0 +1,16 @@
+package modules
+
+type Manifest struct {
+ Name string `json:"name"`
+ Latest string `json:"latest"`
+ Versions map[string]Version `json:"versions"`
+}
+
+type Version struct {
+ Artifacts map[string]Artifact `json:"artifacts"`
+}
+
+type Artifact struct {
+ Url string `json:"url"`
+ Checksum string `json:"checksum"`
+}
From 2cee9886a2f93308ceda68b64df5b63183a45683 Mon Sep 17 00:00:00 2001
From: IdanK
Date: Sat, 1 Aug 2026 16:42:56 +0300
Subject: [PATCH 3/4] Update README.md
---
README.md | 43 ++++++++++++++++++++++++++++++++++++-------
1 file changed, 36 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index cf286f4..c745fd7 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@
- Documentation
+ Website
·
Downloads
·
@@ -24,6 +24,22 @@
---
+## Table of Contents
+
+- [Overview](#overview)
+- [Key Features](#key-features)
+- [Platform Support](#platform-support)
+- [Installation](#installation)
+ - [Install script](#install-script)
+ - [Manual download](#manual-download)
+ - [Build from source](#build-from-source)
+- [Management modules](#management-modules)
+ - [Install](#install)
+ - [Uninstall](#uninstall)
+- [Usage](#usage)
+ - [Example session](#example-session)
+- [Contributing](#contributing)
+
## Overview
**GoScouter** is a modular toolkit for scouting, probing, and analyzing
@@ -36,7 +52,7 @@ Built with a focus on speed and extensibility, GoScouter is designed to grow
with your workflow: enable only the modules you need, script it into your
pipelines, and extend it with your own probes.
-> ⚠️ **Use responsibly.** GoScouter is intended for authorized security
+> **Use responsibly.** GoScouter is intended for authorized security
> testing, research, and network administration only. Always ensure you have
> explicit permission to scan and probe the networks and hosts you target.
@@ -54,11 +70,11 @@ pipelines, and extend it with your own probes.
machine-friendly formats for scripting and automation.
## Platform Support
-
-| Platform | Supported | Notes |
-| -------- | :-------: | ------------------------------ |
-| Linux | ✅ | Primary development platform |
-| macOS | ✅ | Intel & Apple Silicon |
+| Platform | Status |
+| -------- | :----: |
+| Linux | OK |
+| macOS | OK |
+| Windows | N/A |
## Installation
@@ -133,6 +149,19 @@ platform, use the `release-build` target:
make release-build GOOS=linux GOARCH=amd64 # dist/gs-linux-amd64
make release-build GOOS=darwin GOARCH=arm64 # dist/gs-darwin-arm64
```
+## Management modules
+
+### Install
+```bash
+gs --install
+```
+
+If version attribute is absent then it takes the `latest` version of the module, that is specified in `manifest.json`
+
+### Uninstall
+```bash
+gs --uninstall
+```
## Usage
From a5abf8da3e99522ec5b54f96b82560467449a77e Mon Sep 17 00:00:00 2001
From: IdanK
Date: Sat, 1 Aug 2026 16:48:38 +0300
Subject: [PATCH 4/4] Add CHANGELOG.md
---
.github/workflows/ci.yml | 34 ++++++++++++++++++++++-
CHANGELOG.md | 59 ++++++++++++++++++++++++++++++++++++++++
2 files changed, 92 insertions(+), 1 deletion(-)
create mode 100644 CHANGELOG.md
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f9effc2..02f3782 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -134,12 +134,44 @@ jobs:
working-directory: dist/bin
run: sha256sum gs-* > checksums.txt
+ - name: Extract changelog section
+ env:
+ VERSION: ${{ github.event.inputs.version }}
+ run: |
+ set -euo pipefail
+ section="${VERSION#v}"
+
+ # Print everything between "## []" and the next "## [" heading,
+ # stopping early at the link-reference definitions at the bottom.
+ awk -v ver="$section" '
+ /^## \[/ {
+ if (found) exit
+ heading = $0
+ sub(/^## \[/, "", heading)
+ sub(/\].*$/, "", heading)
+ if (heading == ver) found = 1
+ next
+ }
+ found && /^\[[^]]+\]:/ { exit }
+ found { print }
+ ' CHANGELOG.md \
+ | sed -e '/./,$!d' | tac | sed -e '/./,$!d' | tac > release-notes.md
+
+ if [ ! -s release-notes.md ]; then
+ echo "::error::No CHANGELOG.md section for '${section}'. Add a '## [${section}] - ' entry before releasing."
+ exit 1
+ fi
+
+ echo "Release notes for ${VERSION}:"
+ cat release-notes.md
+
- name: Publish release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.inputs.version }}
name: ${{ github.event.inputs.version }}
+ body_path: release-notes.md
generate_release_notes: true
files: |
dist/bin/gs-*
- dist/checksums.txt
+ dist/bin/checksums.txt
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..8f1ea99
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,59 @@
+# Changelog
+
+All notable changes to GoScouter are documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Added
+
+- Module management from the CLI: `gs --install ` and
+ `gs --uninstall `. When the version is omitted, the `latest`
+ version declared in the module's `manifest.json` is used.
+- Module dependency graph resolution, so modules can depend on other modules
+ and are loaded in the right order.
+- PDF summary output for network scans.
+- Community and contribution scaffolding: `CONTRIBUTING.md`, issue templates,
+ and a PR review request workflow.
+
+### Changed
+
+- Rewrote the module subsystem around a central manager and runner, replacing
+ the previous per-command install/uninstall/scan implementations.
+- Replaced the standalone `internal/logger` package with output helpers in
+ `internal/style`, unifying how commands and modules print to the terminal.
+- Reworked DNS, HTTP, and subdomain record handling in `pkg/records` and
+ `pkg/subdomains`.
+- Documentation updates to `README.md` covering installation, module
+ management, and usage.
+
+### Removed
+
+- `internal/logger` — superseded by the styled output helpers.
+
+## [1.0.0] - 2026-07-18
+
+First stable release.
+
+### Added
+
+- Interactive shell interface: run `gs --target ` to drop into the `gs>`
+ prompt, with built-in `help`, `info`, `install`, and `exit` commands.
+- Modular architecture — DNS, HTTP, and subdomain probing each live in their
+ own module, loaded on demand.
+- Cross-platform binaries for Linux and macOS.
+- Install script (`scripts/install.sh`) that selects the right prebuilt binary
+ for the platform, verifies its SHA-256 checksum, and installs it to
+ `/usr/local/bin` or `~/.local/bin`.
+- `make build` and `make release-build` targets for building from source and
+ cross-compiling release artifacts.
+
+### Changed
+
+- Complete rewrite of the project on top of the standard Go project layout
+ (`cmd/`, `internal/`, `pkg/`).
+
+[Unreleased]: https://github.com/GoScouter/GoScouter/compare/1.0.0...HEAD
+[1.0.0]: https://github.com/GoScouter/GoScouter/compare/2026-b1-rc1...1.0.0