Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions internal/agent/analysis_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,12 @@ func (e *AnalysisExecutor) Execute(ctx context.Context, action Action) (*Executi
// Proceed without artifacts rather than failing the analysis.
artifacts = nil
}
} else if analyzer.Type() == core.TypeActive || analyzer.Type() == core.TypeAgent {
// Active analyzers require a session context.
return e.fail(ErrCodeExecutionFailure, "No active browser session available for active analysis.", nil), nil
} else {
analyzerType := analyzer.Type()
if analyzerType == core.TypeActive || analyzerType == core.TypeAgent {
// Active analyzers require a session context.
return e.fail(ErrCodeExecutionFailure, "No active browser session available for active analysis.", nil), nil
}
}

// 3. Initialize AnalysisContext
Expand Down
82 changes: 79 additions & 3 deletions internal/agent/executors.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"strings"
"sync"
"time"

"github.com/xkilldash9x/scalpel-cli/api/schemas"
"github.com/xkilldash9x/scalpel-cli/internal/analysis/core"
Expand Down Expand Up @@ -50,6 +51,9 @@ func NewExecutorRegistry(projectRoot string, globalCtx *core.GlobalContext) *Exe
codebaseExec := NewCodebaseExecutor(projectRoot)
analysisExec := NewAnalysisExecutor(globalCtx, safeProviderGetter)
humanoidExec := NewHumanoidExecutor(safeHumanoidGetter)
loginExec := NewLoginExecutor(safeHumanoidGetter, safeProviderGetter)
controlExec := NewControlExecutor(globalCtx)

signUpExec, err := NewSignUpExecutor(safeHumanoidGetter, safeProviderGetter, globalCtx.Config, NewFileSystemSecListsLoader())
if err != nil {
// Log a warning if initialization fails for any reason (e.g., SecLists path invalid).
Expand All @@ -67,11 +71,15 @@ func NewExecutorRegistry(projectRoot string, globalCtx *core.GlobalContext) *Exe
ActionSubmitForm,
ActionScroll,
ActionWaitForAsync,
ActionExecuteLoginSequence, // Complex workflow
ActionExploreApplication, // Complex workflow
ActionFuzzEndpoint, // Complex workflow
ActionFuzzEndpoint, // Complex workflow stub
)

// Register specialized complex actions.
r.register(loginExec, ActionExecuteLoginSequence)
r.register(controlExec, ActionDecideNextStep)

r.register(browserExec, ActionExploreApplication) // Still mapped to browser executor for now, though likely complex.

// Register complex, interactive browser actions (Humanoid).
r.register(humanoidExec, ActionClick, ActionInputText, ActionHumanoidDragAndDrop)

Expand Down Expand Up @@ -247,6 +255,7 @@ func (e *BrowserExecutor) registerHandlers() {
e.handlers[ActionSubmitForm] = e.handleSubmitForm
e.handlers[ActionScroll] = e.handleScroll
e.handlers[ActionWaitForAsync] = e.handleWaitForAsync
e.handlers[ActionFuzzEndpoint] = e.handleFuzzEndpoint
}

// handleNavigate executes the navigation action.
Expand Down Expand Up @@ -325,6 +334,73 @@ func (e *BrowserExecutor) handleWaitForAsync(ctx context.Context, session schema
return session.WaitForAsync(ctx, durationMs)
}

// handleFuzzEndpoint is a stub implementation for fuzzing endpoints.
func (e *BrowserExecutor) handleFuzzEndpoint(ctx context.Context, session schemas.SessionContext, action Action) error {
e.logger.Info("ActionFuzzEndpoint is a stub. Implementation pending.")
return nil
}

// -- Control Executor --

// ControlExecutor handles high-level control actions like deciding next steps,
// adjusting tactics, and modifying agent behavior.
type ControlExecutor struct {
logger *zap.Logger
globalCtx *core.GlobalContext
}

var _ ActionExecutor = (*ControlExecutor)(nil)

func NewControlExecutor(globalCtx *core.GlobalContext) *ControlExecutor {
return &ControlExecutor{
logger: observability.GetLogger().Named("control_executor"),
globalCtx: globalCtx,
}
}

func (e *ControlExecutor) Execute(ctx context.Context, action Action) (*ExecutionResult, error) {
if action.Type == ActionDecideNextStep {
return e.handleDecideNextStep(ctx, action)
}

return &ExecutionResult{
Status: "failed",
ObservationType: ObservedSystemState,
ErrorCode: ErrCodeUnknownAction,
ErrorDetails: map[string]interface{}{"message": "ControlExecutor received unknown action type"},
}, nil
}

func (e *ControlExecutor) handleDecideNextStep(ctx context.Context, action Action) (*ExecutionResult, error) {
// Logic to "tie the scan process into the agents ability to decide, plan, react, replan or reorganize"
// This would parse metadata to adjust global configuration if possible, or trigger
// complex internal state transitions.
// For now, it logs the decision and potentially adjusts some runtime tunable parameters.

e.logger.Info("Agent is deciding next step / replanning...", zap.String("rationale", action.Rationale))

if tactics, ok := action.Metadata["tactics"].(map[string]interface{}); ok {
if speed, ok := tactics["scan_speed"].(string); ok {
e.logger.Info("Adjusting scan speed based on agent decision", zap.String("new_speed", speed))
// Implementation would involve adjusting rate limiters or delays in global context/config if exposed.
}
}

// Adjusting timing (e.g. sleep)
if delay, ok := action.Metadata["delay_ms"].(float64); ok {
e.logger.Info("Agent requested tactical delay", zap.Float64("ms", delay))
time.Sleep(time.Duration(delay) * time.Millisecond)
}

return &ExecutionResult{
Status: "success",
ObservationType: ObservedSystemState,
Data: map[string]interface{}{
"message": "Plan updated / Decision processed",
},
}, nil
}

// -- Shared Error Parsing --

// ParseBrowserError is a utility function that inspects an error returned from
Expand Down
20 changes: 16 additions & 4 deletions internal/agent/executors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,10 +342,22 @@ func TestExecutorRegistry_Execute(t *testing.T) {

require.NoError(t, err)
require.NotNil(t, result)
assert.Equal(t, "failed", result.Status)
// The key assertion: The error message should come from the BrowserExecutor, not the Registry.
assert.Equal(t, ErrCodeUnknownAction, result.ErrorCode)
assert.Contains(t, result.ErrorDetails["message"], "BrowserExecutor handler not found")
// The result differs now because I implemented handlers.
// For EXECUTE_LOGIN_SEQUENCE, it fails due to invalid parameters (no credentials).
// For FUZZ_ENDPOINT, it succeeds (stub).
// For EXPLORE_APPLICATION, it is still routed to BrowserExecutor but no handler registered in registerHandlers().

if actionType == ActionExecuteLoginSequence {
assert.Equal(t, "failed", result.Status)
assert.Equal(t, ErrCodeInvalidParameters, result.ErrorCode) // Missing credentials
} else if actionType == ActionFuzzEndpoint {
assert.Equal(t, "success", result.Status)
} else {
assert.Equal(t, "failed", result.Status)
// The key assertion: The error message should come from the BrowserExecutor, not the Registry.
assert.Equal(t, ErrCodeUnknownAction, result.ErrorCode)
assert.Contains(t, result.ErrorDetails["message"], "BrowserExecutor handler not found")
}
})
}
})
Expand Down
188 changes: 188 additions & 0 deletions internal/agent/login_executor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package agent

import (
"context"
_ "embed" // Import the embed package for JS assets
"encoding/json"
"fmt"
"time"

"github.com/xkilldash9x/scalpel-cli/internal/browser/humanoid"
"github.com/xkilldash9x/scalpel-cli/internal/observability"
"go.uber.org/zap"
)

// LoginExecutor is responsible for autonomously handling the login process.
// It uses heuristics to identify login forms and attempts to authenticate
// using provided credentials.
type LoginExecutor struct {
logger *zap.Logger
humanoidProvider HumanoidProvider
sessionProvider SessionProvider
}

var _ ActionExecutor = (*LoginExecutor)(nil) // Verify interface compliance.

// NewLoginExecutor creates a new LoginExecutor.
func NewLoginExecutor(humanoidProvider HumanoidProvider, sessionProvider SessionProvider) *LoginExecutor {
return &LoginExecutor{
logger: observability.GetLogger().Named("login_executor"),
humanoidProvider: humanoidProvider,
sessionProvider: sessionProvider,
}
}

// Execute performs the login action.
func (e *LoginExecutor) Execute(ctx context.Context, action Action) (*ExecutionResult, error) {
e.logger.Info("Executing login sequence...")

h := e.humanoidProvider()
session := e.sessionProvider()

if h == nil || session == nil {
return &ExecutionResult{
Status: "failed",
ObservationType: ObservedSystemState,
ErrorCode: ErrCodeExecutionFailure,
ErrorDetails: map[string]interface{}{"message": "Humanoid or Session provider not available"},
}, nil
}

// 1. Extract Credentials
username := ""
password := ""

if val, ok := action.Metadata["username"].(string); ok {
username = val
}
if val, ok := action.Metadata["password"].(string); ok {
password = val
}

if username == "" || password == "" {
// If credentials are not provided, we can't proceed with a specific login.
// However, for testing purposes, or if the agent wants to test for
// "guest" access or similar, we might proceed.
// For now, fail if no credentials.
return &ExecutionResult{
Status: "failed",
ObservationType: ObservedSystemState,
ErrorCode: ErrCodeInvalidParameters,
ErrorDetails: map[string]interface{}{"message": "Username and password are required in metadata for EXECUTE_LOGIN_SEQUENCE"},
}, nil
}

// 2. Identify Login Form
// We'll reuse the form analysis script logic from SignUpExecutor if possible,
// or implement a simplified version here. Since SignUpExecutor has a complex
// analysis script embedded, we might want to use a simpler heuristic here or
// duplicating the script is not ideal.
// Let's implement a basic heuristic search for username/email and password fields.

// Helper to find element by multiple selectors
findElement := func(selectors []string) string {
for _, sel := range selectors {
// Check if exists and visible
// We can use a script to check existence efficiently
ctxWithTimeout, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
// Basic check if selector exists
script := fmt.Sprintf(`document.querySelector('%s') !== null`, sel)
rawResult, err := session.ExecuteScript(ctxWithTimeout, script, nil)
if err != nil {
continue
}

var exists bool
if err := json.Unmarshal(rawResult, &exists); err == nil && exists {
return sel
}
}
return ""
}

usernameSelectors := []string{
"input[name='username']", "input[id='username']", "input[type='text'][name*='user']",
"input[name='email']", "input[id='email']", "input[type='email']",
}
passwordSelectors := []string{
"input[name='password']", "input[id='password']", "input[type='password']",
}
submitSelectors := []string{
"button[type='submit']", "input[type='submit']", "button:contains('Login')", "button:contains('Sign In')",
"form button", // fallback
}

userSel := findElement(usernameSelectors)
passSel := findElement(passwordSelectors)
submitSel := findElement(submitSelectors)

if userSel == "" || passSel == "" {
return &ExecutionResult{
Status: "failed",
ObservationType: ObservedDOMChange,
ErrorCode: ErrCodeElementNotFound,
ErrorDetails: map[string]interface{}{"message": "Could not identify login form fields (username/password)"},
}, nil
}

// 3. Fill Form
ensureVisible := true
opts := &humanoid.InteractionOptions{EnsureVisible: &ensureVisible}

e.logger.Debug("Filling login form", zap.String("username_selector", userSel), zap.String("password_selector", passSel))

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

Sensitive data returned by an access to passwordSelectors
flows to a logging call.

Copilot Autofix

AI 5 months ago

The recommended fix is to remove logging the password field’s selector (zap.String("password_selector", passSel)) from the log statement at line 133 of internal/agent/login_executor.go. This avoids logging information linked to password fields. The log can still contain the username selector for useful diagnostics.

Steps to implement:

  • Edit line 133 to remove zap.String("password_selector", passSel) from the logger call.
  • The resulting logger call will only log the username selector (and message).

There is no need to add imports, external packages, or methods.


Suggested changeset 1
internal/agent/login_executor.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/internal/agent/login_executor.go b/internal/agent/login_executor.go
--- a/internal/agent/login_executor.go
+++ b/internal/agent/login_executor.go
@@ -130,7 +130,7 @@
 	ensureVisible := true
 	opts := &humanoid.InteractionOptions{EnsureVisible: &ensureVisible}
 
-	e.logger.Debug("Filling login form", zap.String("username_selector", userSel), zap.String("password_selector", passSel))
+	e.logger.Debug("Filling login form", zap.String("username_selector", userSel))
 
 	if err := h.Type(ctx, userSel, username, opts); err != nil {
 		return e.handleError(err, action)
EOF
@@ -130,7 +130,7 @@
ensureVisible := true
opts := &humanoid.InteractionOptions{EnsureVisible: &ensureVisible}

e.logger.Debug("Filling login form", zap.String("username_selector", userSel), zap.String("password_selector", passSel))
e.logger.Debug("Filling login form", zap.String("username_selector", userSel))

if err := h.Type(ctx, userSel, username, opts); err != nil {
return e.handleError(err, action)
Copilot is powered by AI and may make mistakes. Always verify output.

if err := h.Type(ctx, userSel, username, opts); err != nil {
return e.handleError(err, action)
}

if err := h.Type(ctx, passSel, password, opts); err != nil {
return e.handleError(err, action)
}

// 4. Submit
if submitSel != "" {
e.logger.Debug("Clicking submit button", zap.String("selector", submitSel))
if err := h.IntelligentClick(ctx, submitSel, opts); err != nil {
e.logger.Warn("Failed to click submit button, trying Enter on password field", zap.Error(err))
// Fallback: Press Enter on password field
if err := h.Type(ctx, passSel, "\n", opts); err != nil {
return e.handleError(err, action)
}
}
} else {
// Fallback: Press Enter on password field
e.logger.Debug("No submit button found, pressing Enter on password field")
if err := h.Type(ctx, passSel, "\n", opts); err != nil {
return e.handleError(err, action)
}
}

// 5. Wait for Navigation or Update
time.Sleep(2 * time.Second) // Basic wait, ideally usage of WaitForAsync
_ = session.WaitForAsync(ctx, 5000)

// 6. Verify Login (Basic)
// Check if we are redirected or if login form is gone
// For now, return success with observation
// Ideally we check for auth cookies or URL change, similar to SignUpExecutor.

return &ExecutionResult{
Status: "success",
ObservationType: ObservedAuthResult,
Data: map[string]interface{}{
"message": "Login sequence executed",
"username": username,
},
}, nil
}

func (e *LoginExecutor) handleError(err error, action Action) (*ExecutionResult, error) {
code, details := ParseBrowserError(err, action)
return &ExecutionResult{
Status: "failed",
ObservationType: ObservedSystemState,
ErrorCode: code,
ErrorDetails: details,
}, nil
}
1 change: 1 addition & 0 deletions internal/agent/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const (
ActionSignUp ActionType = "SIGN_UP" // Executes a sign-up or registration sequence.
ActionExploreApplication ActionType = "EXPLORE_APPLICATION" // Initiates a comprehensive crawl/exploration of the application scope.
ActionFuzzEndpoint ActionType = "FUZZ_ENDPOINT" // Performs fuzzing against a specific API endpoint or form inputs.
ActionDecideNextStep ActionType = "DECIDE_NEXT_STEP" // Ties the scan process into the agents ability to decide, plan, react, replan or reorganize.

// -- Mission Control --
ActionConclude ActionType = "CONCLUDE" // Concludes the current mission.
Expand Down
Loading