Thank you for your interest in contributing to Brutus! This document provides guidelines and instructions for contributing.
- Code of Conduct
- Getting Started
- Development Setup
- Making Changes
- Adding a New Protocol
- Testing
- Pull Request Process
- Style Guide
By participating in this project, you agree to maintain a respectful and inclusive environment. We expect all contributors to:
- Use welcoming and inclusive language
- Respect differing viewpoints and experiences
- Accept constructive criticism gracefully
- Focus on what's best for the community
- Go 1.22 or later
- Git
- Docker (for integration tests)
- Make (optional, for convenience commands)
- Fork the repository on GitHub
- Clone your fork:
git clone https://github.com/YOUR_USERNAME/brutus.git
cd brutus- Add upstream remote:
git remote add upstream https://github.com/praetorian-inc/brutus.gitgo mod download# Run unit tests
go test -short ./...
# Run linter
golangci-lint rundocker compose up -dAlways create a feature branch for your changes:
git checkout -b feature/your-feature-name
# or
git checkout -b fix/your-bug-fixFollow conventional commit format:
type(scope): description
[optional body]
[optional footer]
Types:
feat: New featurefix: Bug fixdocs: Documentation onlystyle: Code style changes (formatting, etc.)refactor: Code refactoringtest: Adding or updating testschore: Maintenance tasks
Examples:
feat(ssh): add support for ed25519 keys
fix(mysql): handle connection timeout correctly
docs(readme): add installation instructions
test(ftp): add integration tests
- One logical change per commit
- Keep commits small and reviewable
- Squash WIP commits before submitting PR
Adding a new protocol plugin involves these steps:
mkdir -p internal/plugins/yourprotocolCreate internal/plugins/yourprotocol/yourprotocol.go:
// Copyright 2026 Praetorian Security, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package yourprotocol
import (
"context"
"fmt"
"time"
"github.com/praetorian-inc/brutus/pkg/brutus"
)
func init() {
brutus.Register("yourprotocol", func() brutus.Plugin {
return &Plugin{}
})
}
// Plugin implements YourProtocol password authentication.
type Plugin struct{}
// Name returns the protocol name.
func (p *Plugin) Name() string {
return "yourprotocol"
}
// Test attempts authentication using the provided credentials.
//
// Returns Result with:
// - Success=true, Error=nil: Valid credentials
// - Success=false, Error=nil: Invalid credentials (auth failure)
// - Success=false, Error!=nil: Connection/network error
func (p *Plugin) Test(ctx context.Context, target, username, password string,
timeout time.Duration) *brutus.Result {
start := time.Now()
result := &brutus.Result{
Protocol: "yourprotocol",
Target: target,
Username: username,
Password: password,
Success: false,
}
// Parse target with IPv6 support (uses default port if not specified)
host, port := brutus.ParseTarget(target, "1234")
// TODO: Implement authentication logic using host, port, username, password
// On error: result.Error = classifyError(err)
// On success: result.Success = true
_ = host
_ = port
result.Duration = time.Since(start)
return result
}Use the shared brutus.ClassifyAuthError helper to distinguish authentication failures from connection errors. Define your protocol's auth indicators and delegate to the shared classifier:
var yourprotocolAuthIndicators = []string{
"authentication failed",
"access denied",
// Add protocol-specific error patterns that indicate wrong credentials
}
// classifyError classifies protocol-specific errors.
// Uses shared brutus.ClassifyAuthError with protocol auth indicators
// to distinguish authentication failures from connection errors.
func classifyError(err error) error {
return brutus.ClassifyAuthError(err, yourprotocolAuthIndicators)
}The shared helper handles nil checks, case-insensitive matching, and wraps non-auth errors as "connection error: ...". See existing plugins (e.g., mysql, ssh, redis) for examples of protocol-specific indicators.
Create internal/plugins/yourprotocol/yourprotocol_test.go:
package yourprotocol
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestPlugin_Name(t *testing.T) {
p := &Plugin{}
assert.Equal(t, "yourprotocol", p.Name())
}
func TestPlugin_Test_ErrorClassification(t *testing.T) {
tests := []struct {
name string
errStr string
wantAuth bool // true if should be classified as auth error (nil)
}{
{
name: "authentication failed",
errStr: "authentication failed for user",
wantAuth: true,
},
{
name: "access denied",
errStr: "access denied",
wantAuth: true,
},
{
name: "connection error",
errStr: "connection refused",
wantAuth: false,
},
{
name: "timeout error",
errStr: "context deadline exceeded",
wantAuth: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := &mockError{msg: tt.errStr}
result := classifyError(err)
if tt.wantAuth {
assert.Nil(t, result, "auth errors should return nil")
} else {
assert.NotNil(t, result, "connection errors should be wrapped")
assert.Contains(t, result.Error(), "connection error")
}
})
}
}
func TestPlugin_Test_ConnectionRefused(t *testing.T) {
p := &Plugin{}
ctx := context.Background()
result := p.Test(ctx, "localhost:9999", "user", "pass", 2*time.Second)
assert.NotNil(t, result)
assert.Equal(t, "yourprotocol", result.Protocol)
assert.False(t, result.Success, "Expected connection failure")
assert.NotNil(t, result.Error, "Connection error should have non-nil error")
assert.Contains(t, result.Error.Error(), "connection error")
assert.Greater(t, result.Duration, time.Duration(0))
}
func TestPlugin_Test_ValidCredentials(t *testing.T) {
t.Skip("Integration test - requires YourProtocol server")
// TODO: Add integration test with real server
}
func TestPlugin_Test_InvalidCredentials(t *testing.T) {
t.Skip("Integration test - requires YourProtocol server")
// TODO: Add integration test with real server
}
// mockError is a simple error implementation for testing error classification
type mockError struct {
msg string
}
func (e *mockError) Error() string {
return e.msg
}Create wordlists/yourprotocol_defaults.txt:
# YourProtocol default credentials
# Format: username:password
admin:admin
admin:password
root:root
Add import to pkg/builtins/builtins.go:
package builtins
import (
_ "github.com/praetorian-inc/brutus/internal/plugins/ssh"
_ "github.com/praetorian-inc/brutus/internal/plugins/ftp"
// ... other plugins
_ "github.com/praetorian-inc/brutus/internal/plugins/yourprotocol"
)Add your protocol to the Supported Protocols table in README.md.
go test ./... -vgo test -short ./...# Start test services
docker compose up -d
# Run integration tests
go test -tags=integration ./... -v
# Cleanup
docker compose downgo test -coverprofile=coverage.out ./... -short
go tool cover -func=coverage.out
# Generate HTML report
go tool cover -html=coverage.out -o coverage.html- Minimum coverage: 80% for new code
- Core packages: 85%+ coverage
- All error paths must be tested
golangci-lint run-
Sync with upstream:
git fetch upstream git rebase upstream/main
-
Run all checks:
go test -short ./... golangci-lint run -
Update documentation if needed
-
Add tests for new functionality
-
Push your branch:
git push origin feature/your-feature-name
-
Create a Pull Request on GitHub
-
Fill out the PR template:
- Description of changes
- Related issues
- Testing performed
- Breaking changes (if any)
-
Automated checks must pass:
- CI build
- Lint checks
- Test suite
- Coverage threshold
-
Code review by maintainer
-
Address feedback with additional commits
-
Squash and merge when approved
Use conventional commit format:
feat(protocol): add support for XYZ
fix(ssh): handle timeout correctly
- Use
gofmtfor formatting - Use
goimportsfor import organization - Run
golangci-lintbefore committing
Organize imports in three groups:
- Standard library
- External packages
- Internal packages
import (
"context"
"fmt"
"time"
"github.com/jlaffaye/ftp"
"github.com/praetorian-inc/brutus/pkg/brutus"
)- Always handle errors explicitly
- Wrap errors with context:
return fmt.Errorf("connection error: %w", err)
- Use the error classification pattern for auth vs connection errors
- Add package-level documentation
- Document exported functions and types
- Use complete sentences
- Explain "why" not "what"
// Plugin implements FTP password authentication.
// It uses the jlaffaye/ftp library for RFC 959 compliance.
type Plugin struct{}
// Test attempts FTP authentication using the provided credentials.
//
// Returns Result with:
// - Success=true, Error=nil: Valid credentials
// - Success=false, Error=nil: Invalid credentials (auth failure)
// - Success=false, Error!=nil: Connection/network error
func (p *Plugin) Test(...) *brutus.Result {- Use descriptive names
- Avoid abbreviations (except common ones like
ctx,err) - Plugin types should be named
Plugin - Test functions should be
Test<Function>_<Scenario>
- Use table-driven tests where appropriate
- Test both success and failure paths
- Mock external services in unit tests
- Use integration tests for real service testing
- Open an issue for questions
- Join discussions on GitHub
- Review existing PRs and issues for context
Thank you for contributing to Brutus!