Skip to content

Commit ce063d0

Browse files
committed
modified: .gitignore
modified: README.md modified: cmd/scan.go modified: cmd/scan_test.go modified: go.mod modified: go.sum modified: internal/config/config.go modified: internal/config/config_test.go modified: internal/observability/logger.go modified: internal/orchestrator/orchestrator.go modified: internal/orchestrator/orchestrator_test.go modified: internal/worker/adapters/ato_adapter.go modified: internal/worker/adapters/ato_adapter_test.go
1 parent c31c094 commit ce063d0

13 files changed

Lines changed: 246 additions & 20 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ logs/
2222
# Local configuration overrides and environment files
2323
# It's good practice to commit config.yaml as a template,
2424
# but ignore local versions with secrets.
25-
config.local.yaml
25+
config.yaml
2626
*.local.yaml
2727
.credentials.json
2828
.env

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Follow these instructions to get Scalpel CLI set up and ready to run your first
4141
* [Go](https://go.dev/doc/install) (version 1.21 or later)
4242
* [Docker](https://docs.docker.com/get-docker/)
4343
* An API key for your chosen LLM provider (e.g., Google AI Studio, OpenAI).
44+
* [SecLists](https://github.com/danielmiessler/SecLists): Required for certain active scanners like the Account Takeover (ATO) module.
4445

4546
### 1. Set Up the PostgreSQL Database
4647

@@ -81,6 +82,14 @@ Create a `config.yaml` file in the root of the project. You can use `config.yaml
8182
database:
8283
url: "postgres://scalpel:secret@localhost:5432/scalpel_db?sslmode=disable"
8384

85+
scanners:
86+
active:
87+
auth:
88+
ato:
89+
# Path to your local SecLists repository.
90+
# It's recommended to clone it to your home directory.
91+
# git clone https://github.com/danielmiessler/SecLists.git ~/SecLists
92+
seclists_path: "~/SecLists"
8493
agent:
8594
llm:
8695
default_fast_model: "gemini-1.5-flash"
@@ -218,4 +227,4 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
218227
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
219228
SOFTWARE.
220229
```
221-
</details>
230+
</details>

cmd/scan.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,17 @@ func newScanCmd(factory ComponentFactory) *cobra.Command {
4343
Short: "Starts a new security scan against the specified targets",
4444
Args: cobra.MinimumNArgs(1),
4545
RunE: func(cmd *cobra.Command, args []string) error {
46+
// Get the original context from the command.
4647
ctx := cmd.Context()
48+
49+
// Retrieve the value of the verbose flag.
50+
verbose, _ := cmd.Flags().GetBool("verbose")
51+
52+
// Add the verbose flag's value to the context. This makes it accessible
53+
// to downstream functions like runScan without needing to pass it as an
54+
// explicit argument. This is a clean way to provide request-scoped values.
55+
ctx = context.WithValue(ctx, "verbose", verbose)
56+
4757
logger := observability.GetLogger()
4858

4959
cfg, err := getConfigFromContext(ctx)
@@ -70,6 +80,7 @@ func newScanCmd(factory ComponentFactory) *cobra.Command {
7080
scanCmd.Flags().IntP("depth", "d", 0, "Maximum crawl depth. (Overrides config/env)")
7181
scanCmd.Flags().IntP("concurrency", "j", 0, "Number of concurrent engine workers. (Overrides config/env)")
7282
scanCmd.Flags().String("scope", "strict", "Scan scope strategy ('strict' or 'subdomain'). (Overrides config/env)")
83+
scanCmd.Flags().BoolP("verbose", "v", false, "Enable verbose (DEBUG) logging.")
7384

7485
return scanCmd
7586
}
@@ -163,6 +174,21 @@ func runScan(
163174
cancelScan()
164175
}()
165176

177+
// --- Logger Re-initialization ---
178+
// Re-initialize the logger based on the final, flag-overridden config.
179+
// This ensures that all subsequent logs (including component initialization)
180+
// respect the verbosity level set by the user.
181+
// We get the verbose flag directly from the command context.
182+
verbose, _ := ctx.Value("verbose").(bool)
183+
if verbose {
184+
loggerCfg := cfg.Logger()
185+
loggerCfg.Level = "debug"
186+
observability.ResetForTest() // Reset to allow re-initialization
187+
observability.InitializeLogger(loggerCfg) // Re-initialize with new level
188+
logger = observability.GetLogger() // Get the new logger instance
189+
logger.Debug("Verbose logging enabled via --verbose flag.") // First debug message
190+
}
191+
166192
// --- Target Validation ---
167193
scanTargets, err := normalizeTargets(targets)
168194
if err != nil {

cmd/scan_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,9 @@ func TestRunScanLogic(t *testing.T) {
215215
// Expect all targets missing a scheme to default to https.
216216
expectedTargets := []string{"https://example.com", "http://test.com", "https://another.org"}
217217

218-
// Factory is called with original targets for initialization logic (like scope).
219-
mockFactory.On("Create", mock.Anything, cfg, targetsInput, mock.AnythingOfType("*zap.Logger")).Return(mockComponents, nil)
218+
// FIX: The factory should be set up with the *expected* normalized targets,
219+
// as this is what the `Create` method will receive after normalization.
220+
mockFactory.On("Create", mock.Anything, cfg, expectedTargets, mock.AnythingOfType("*zap.Logger")).Return(mockComponents, nil)
220221
// Assert that the normalized URLs are passed to the orchestrator.
221222
mockOrchestrator.On("StartScan", mock.Anything, expectedTargets, mock.AnythingOfType("string")).Return(nil)
222223

go.mod

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ require (
1515
github.com/google/uuid v1.6.0
1616
github.com/hpcloud/tail v1.0.0
1717
github.com/jackc/pgx/v5 v5.7.6
18+
github.com/mitchellh/go-homedir v1.1.0
1819
github.com/spf13/cobra v1.10.1
1920
github.com/spf13/viper v1.21.0
2021
github.com/stretchr/testify v1.11.1
2122
go.uber.org/zap v1.27.0
22-
golang.org/x/net v0.46.0
23+
golang.org/x/net v0.47.0
2324
golang.org/x/sync v0.18.0
2425
golang.org/x/time v0.14.0
2526
golang.org/x/tools v0.38.0
@@ -75,10 +76,10 @@ require (
7576
go.uber.org/goleak v1.3.0
7677
go.uber.org/multierr v1.11.0 // indirect
7778
go.yaml.in/yaml/v3 v3.0.4 // indirect
78-
golang.org/x/crypto v0.43.0 // indirect; indirect
79-
golang.org/x/mod v0.29.0 // indirect
79+
golang.org/x/crypto v0.44.0 // indirect; indirect
80+
golang.org/x/mod v0.30.0 // indirect
8081
golang.org/x/sys v0.38.0 // indirect
81-
golang.org/x/text v0.30.0 // indirect
82+
golang.org/x/text v0.31.0 // indirect
8283
gopkg.in/fsnotify.v1 v1.4.7 // indirect
8384
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
8485
gopkg.in/warnings.v0 v0.1.2 // indirect

go.sum

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
100100
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
101101
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
102102
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
103+
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
104+
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
103105
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
104106
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
105107
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -166,13 +168,19 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
166168
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
167169
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
168170
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
171+
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
172+
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
169173
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
170174
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
171175
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
172176
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
177+
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
178+
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
173179
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
174180
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
175181
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
182+
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
183+
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
176184
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
177185
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
178186
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -187,9 +195,12 @@ golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
187195
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
188196
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
189197
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
198+
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
190199
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
191200
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
192201
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
202+
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
203+
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
193204
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
194205
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
195206
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

internal/config/config.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ type AuthConfig struct {
358358
type ATOConfig struct {
359359
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
360360
CredentialFile string `mapstructure:"credential_file" yaml:"credential_file"`
361+
SecListsPath string `mapstructure:"seclists_path" yaml:"seclists_path"`
361362
Concurrency int `mapstructure:"concurrency" yaml:"concurrency"`
362363
MinRequestDelayMs int `mapstructure:"min_request_delay_ms" yaml:"min_request_delay_ms"`
363364
RequestDelayJitterMs int `mapstructure:"request_delay_jitter_ms" yaml:"request_delay_jitter_ms"`
@@ -503,6 +504,13 @@ func SetDefaults(v *viper.Viper) {
503504
v.SetDefault("logger.max_backups", 5)
504505
v.SetDefault("logger.max_age", 30)
505506
v.SetDefault("logger.compress", true)
507+
v.SetDefault("logger.colors.debug", "cyan")
508+
v.SetDefault("logger.colors.info", "green")
509+
v.SetDefault("logger.colors.warn", "yellow")
510+
v.SetDefault("logger.colors.error", "red")
511+
v.SetDefault("logger.colors.dpanic", "magenta")
512+
v.SetDefault("logger.colors.panic", "magenta")
513+
v.SetDefault("logger.colors.fatal", "magenta")
506514

507515
// -- Engine --
508516
v.SetDefault("engine.queue_size", 1000)
@@ -541,6 +549,7 @@ func SetDefaults(v *viper.Viper) {
541549
v.SetDefault("scanners.active.protopollution.wait_duration", 8*time.Second)
542550
v.SetDefault("scanners.active.timeslip.enabled", false)
543551
v.SetDefault("scanners.active.auth.ato.enabled", true)
552+
v.SetDefault("scanners.active.auth.ato.seclists_path", "~/SecLists")
544553
v.SetDefault("scanners.active.auth.idor.enabled", true)
545554

546555
// -- Discovery --

internal/config/config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func TestNewDefaultConfig(t *testing.T) {
2222
assert.True(t, cfg.Browser().Headless)
2323
assert.Equal(t, 30*time.Second, cfg.Network().Timeout)
2424
assert.Equal(t, "postgres", cfg.Agent().KnowledgeGraph.Type)
25-
assert.Equal(t, "gemini-2.5-pro", cfg.Agent().LLM.DefaultPowerfulModel)
25+
assert.Equal(t, "gemini-1.5-pro", cfg.Agent().LLM.DefaultPowerfulModel)
2626
assert.False(t, cfg.Autofix().Enabled)
2727
assert.Equal(t, 0.75, cfg.Autofix().MinConfidenceThreshold)
2828
assert.Equal(t, "scalpel-autofix-bot", cfg.Autofix().Git.AuthorName)

internal/observability/logger.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ func getEncoder(cfg config.LoggerConfig) zapcore.Encoder {
154154

155155
// Customize the encoder to create a clean, single-line log message.
156156
// This avoids the multi-line, key-value output of the default console encoder.
157-
return zapcore.NewConsoleEncoder(encoderConfig)
157+
return newCustomConsoleEncoder(encoderConfig)
158158
}
159159

160160
// --- JSON Format (Default) ---
@@ -164,6 +164,26 @@ func getEncoder(cfg config.LoggerConfig) zapcore.Encoder {
164164
return zapcore.NewJSONEncoder(encoderConfig)
165165
}
166166

167+
// newCustomConsoleEncoder creates a Zap encoder with a custom format optimized for
168+
// human readability in a terminal. It produces a clean, single-line output that
169+
// includes the timestamp, a color-coded level, the logger's name (component),
170+
// the main message, and any structured fields as a JSON blob.
171+
func newCustomConsoleEncoder(cfg zapcore.EncoderConfig) zapcore.Encoder {
172+
// We create a new encoder configuration to avoid modifying the original.
173+
// This ensures that if the original cfg is used elsewhere, it remains unchanged.
174+
consoleCfg := cfg
175+
// The `EncodeName` is customized to add a dot suffix, making the component
176+
// name visually distinct in the log line (e.g., "scalpel-cli.DiscoveryEngine.").
177+
consoleCfg.EncodeName = func(loggerName string, enc zapcore.PrimitiveArrayEncoder) {
178+
enc.AppendString(loggerName + ".")
179+
}
180+
181+
// The custom encoder is built on top of Zap's standard ConsoleEncoder.
182+
// By creating our own implementation, we gain full control over the final
183+
// log format, allowing us to structure the output exactly as desired.
184+
return zapcore.NewConsoleEncoder(consoleCfg)
185+
}
186+
167187
// GetLogger returns the initialized global logger instance.
168188
func GetLogger() *zap.Logger {
169189
logger := globalLogger.Load() // Atomically load the logger pointer.

internal/orchestrator/orchestrator.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,10 @@ func (o *Orchestrator) StartScan(ctx context.Context, targets []string, scanID s
9696
if scanners.Active.Auth.IDOR.Enabled {
9797
o.logger.Info("Dispatching IDOR task")
9898
mergedTaskChan <- schemas.Task{
99-
TaskID: uuid.NewString(),
100-
ScanID: scanID,
101-
Type: schemas.TaskTestAuthIDOR,
99+
TaskID: uuid.NewString(),
100+
ScanID: scanID,
101+
Type: schemas.TaskTestAuthIDOR,
102+
Parameters: schemas.IDORTaskParams{},
102103
}
103104
}
104105
if scanners.Active.Auth.ATO.Enabled {
@@ -107,6 +108,9 @@ func (o *Orchestrator) StartScan(ctx context.Context, targets []string, scanID s
107108
TaskID: uuid.NewString(),
108109
ScanID: scanID,
109110
Type: schemas.TaskTestAuthATO,
111+
Parameters: schemas.ATOTaskParams{
112+
Usernames: []string{},
113+
},
110114
}
111115
}
112116

@@ -116,6 +120,9 @@ func (o *Orchestrator) StartScan(ctx context.Context, targets []string, scanID s
116120
TaskID: uuid.NewString(),
117121
ScanID: scanID,
118122
Type: schemas.TaskAgentMission,
123+
Parameters: schemas.AgentMissionParams{
124+
MissionBrief: "Perform a comprehensive security audit of the target application.",
125+
},
119126
}
120127

121128
// Wait for discovery to finish, then close the merged channel

0 commit comments

Comments
 (0)