Add mysql-scanner: MySQL malware detection tool
This commit is contained in:
11
mysql-scanner/.env.example
Normal file
11
mysql-scanner/.env.example
Normal file
@@ -0,0 +1,11 @@
|
||||
# MySQL Scanner - Environment Variables Example
|
||||
# Copy this to .env and fill in your database credentials
|
||||
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=wp_user
|
||||
DB_PASSWORD=your_password_here
|
||||
DB_NAME=wordpress
|
||||
|
||||
# Optional: Limit patterns (comma-separated)
|
||||
# PATTERNS=eval,base64,script
|
||||
97
mysql-scanner/README.md
Normal file
97
mysql-scanner/README.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# mysql-scanner
|
||||
|
||||
MySQL database malware scanner for WordPress and generic schemas.
|
||||
|
||||
## Purpose
|
||||
|
||||
Detect hacked content and malware in MySQL databases by scanning text columns for:
|
||||
- Base64 encoded payloads
|
||||
- PHP dangerous functions (eval, exec, system, etc.)
|
||||
- HTML/JS injection (script tags, iframes, event handlers)
|
||||
- Obfuscation patterns
|
||||
- Suspicious comment signatures
|
||||
|
||||
## Features
|
||||
|
||||
- **Schema-agnostic**: Analyzes any database structure automatically
|
||||
- **WordPress-aware**: Prioritizes common WordPress tables (wp_posts, wp_options, etc.)
|
||||
- **Pattern library**: Extensible detection patterns
|
||||
- **Contextual reports**: Shows table, column, row ID, and offending snippets
|
||||
- **Risk levels**: High/Medium/Low classification
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cd tools/go/mysql-scanner
|
||||
go build -o mysql-scanner
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Scan WordPress database
|
||||
./mysql-scanner --host localhost --user wp_user --password secret --db wordpress
|
||||
|
||||
# Scan with custom port and pattern filters
|
||||
./mysql-scanner --host localhost --port 3307 --user user --db mydb --patterns eval,base64,script
|
||||
|
||||
# Output JSON for automated processing
|
||||
./mysql-scanner --host localhost --user user --db mydb --json --output scan-results.json
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=username
|
||||
DB_PASSWORD=secret
|
||||
DB_NAME=database
|
||||
```
|
||||
|
||||
## Detection Patterns
|
||||
|
||||
### PHP Malware
|
||||
- `eval()`, `assert()`, `create_function()`
|
||||
- `exec()`, `system()`, `passthru()`, `shell_exec()`
|
||||
- `base64_decode()`, `gzinflate()`, `str_rot13()`
|
||||
- `preg_replace()` with `/e` modifier (deprecated but dangerous)
|
||||
|
||||
### Web Shell Patterns
|
||||
- `$_GET`, `$_POST`, `$_REQUEST` with eval
|
||||
- `error_reporting(0)` followed by obfuscation
|
||||
- Variable function calls (`$func()`, `$a($b)`)
|
||||
|
||||
### HTML/JS Injection
|
||||
- `<script>...</script>` tags
|
||||
- `<iframe>` tags (especially with hidden attributes)
|
||||
- Event handlers: `onerror=`, `onload=`, `onclick=`
|
||||
- `javascript:` protocol in attributes
|
||||
|
||||
### Base64 Payloads
|
||||
- Long base64 strings (>200 chars)
|
||||
- Base64 followed by decode/eval patterns
|
||||
- Double-encoded base64
|
||||
|
||||
## Architecture
|
||||
|
||||
1. **Connection**: MySQL driver with configurable auth
|
||||
2. **Schema Discovery**: Queries `information_schema` for tables and text columns
|
||||
3. **Row Scanning**: Iterates through all rows, extracts text content
|
||||
4. **Pattern Matching**: Runs detection patterns against each text field
|
||||
5. **Reporting**: Aggregates findings with context and risk levels
|
||||
|
||||
## Security Notes
|
||||
|
||||
- This tool requires database credentials with SELECT access
|
||||
- Recommended for incident response and periodic security audits
|
||||
- Can be integrated into CI/CD for automated scanning
|
||||
- **Never expose scan reports publicly** - they may contain sensitive data
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Heuristic scoring for combined risk factors
|
||||
- YARA rule support for advanced signatures
|
||||
- Automatic quarantine/sanitization suggestions
|
||||
- Real-time monitoring mode with change detection
|
||||
- Integration with WordPress security plugins
|
||||
212
mysql-scanner/USAGE.md
Normal file
212
mysql-scanner/USAGE.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# MySQL Scanner - Usage Examples
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic WordPress Scan
|
||||
|
||||
```bash
|
||||
cd tools/go/mysql-scanner
|
||||
|
||||
# Using command-line flags
|
||||
./mysql-scanner --host localhost --user wp_user --password secret --db wordpress
|
||||
|
||||
# Using environment variables
|
||||
DB_HOST=localhost DB_USER=wp_user DB_PASSWORD=secret DB_NAME=wordpress ./mysql-scanner
|
||||
```
|
||||
|
||||
### Filter by Pattern Type
|
||||
|
||||
```bash
|
||||
# Only scan for eval() and base64_decode()
|
||||
./mysql-scanner --host localhost --user user --db mydb --patterns eval,base64
|
||||
|
||||
# Only scan for script tags and iframes
|
||||
./mysql-scanner --host localhost --user user --db mydb --patterns script_tag,iframe_tag
|
||||
|
||||
# List all available patterns (check README.md)
|
||||
```
|
||||
|
||||
### JSON Output
|
||||
|
||||
```bash
|
||||
# Output JSON to stdout
|
||||
./mysql-scanner --host localhost --user user --db mydb --json
|
||||
|
||||
# Save JSON to file
|
||||
./mysql-scanner --host localhost --user user --db mydb --json --output scan-results.json
|
||||
|
||||
# Pretty-print JSON with jq
|
||||
./mysql-scanner --host localhost --user user --db mydb --json | jq '.'
|
||||
```
|
||||
|
||||
## Understanding the Output
|
||||
|
||||
### Human-Readable Output
|
||||
|
||||
```
|
||||
Connected to wp_user@localhost:3306/wordpress
|
||||
Found 12 tables
|
||||
Scanning table: wp_posts
|
||||
Scanning 4 text columns: [post_content post_excerpt post_title post_password]
|
||||
Scanning table: wp_options
|
||||
Scanning 3 text columns: [option_value option_name autoload]
|
||||
|
||||
=== SCAN RESULTS ===
|
||||
Database: wordpress
|
||||
Tables scanned: 12
|
||||
Findings: 3 total (High: 1, Medium: 2, Low: 0)
|
||||
|
||||
🔴 HIGH RISK (1)
|
||||
|
||||
[high] wp_posts.post_content (ID: 42)
|
||||
Pattern: eval_function
|
||||
Match: eval(
|
||||
Snippet: Some text with <?php eval(base64_decode('SGVsbG8=')); ?> malicious code
|
||||
|
||||
🟡 MEDIUM RISK (2)
|
||||
...
|
||||
```
|
||||
|
||||
### JSON Output
|
||||
|
||||
```json
|
||||
{
|
||||
"database": "wordpress",
|
||||
"tables_scanned": 12,
|
||||
"rows_scanned": 3,
|
||||
"findings": [
|
||||
{
|
||||
"table": "wp_posts",
|
||||
"column": "post_content",
|
||||
"row_id": "42",
|
||||
"risk_level": "high",
|
||||
"pattern": "eval_function",
|
||||
"match": "eval(",
|
||||
"snippet": "Some text with <?php eval(base64_decode('SGVsbG8=')); ?>"
|
||||
}
|
||||
],
|
||||
"high_risk_count": 1,
|
||||
"medium_risk_count": 2,
|
||||
"low_risk_count": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### Incident Response
|
||||
|
||||
When you suspect a WordPress compromise:
|
||||
|
||||
```bash
|
||||
# Full scan of the database
|
||||
./mysql-scanner --host localhost --user wp_user --password *** --db wordpress --json > incident-scan.json
|
||||
|
||||
# Check only high-risk patterns first
|
||||
./mysql-scanner --host localhost --user wp_user --password *** --db wordpress --patterns eval,assert,base64_decode,exec,system
|
||||
```
|
||||
|
||||
### Periodic Security Audits
|
||||
|
||||
Set up a cron job for automated scanning:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# /usr/local/bin/wordpress-malware-scan.sh
|
||||
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
OUTPUT_DIR="/var/log/malware-scans"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
for DB in wordpress wordpress_blog wordpress_site2; do
|
||||
echo "Scanning $DB..."
|
||||
./mysql-scanner --host localhost --user scanner_user --password *** \
|
||||
--db "$DB" --json --output "$OUTPUT_DIR/$DB-$DATE.json"
|
||||
done
|
||||
|
||||
# Email if high-risk findings
|
||||
HIGH_COUNT=$(jq '.high_risk_count' "$OUTPUT_DIR"/*-$DATE.json | awk '{s+=$1} END {print s}')
|
||||
if [ "$HIGH_COUNT" -gt 0 ]; then
|
||||
echo "ALERT: $HIGH_COUNT high-risk malware findings detected" | \
|
||||
mail -s "WordPress Malware Scan Results" admin@example.com
|
||||
fi
|
||||
```
|
||||
|
||||
### Development Testing
|
||||
|
||||
Scan development databases before production deployment:
|
||||
|
||||
```bash
|
||||
# Scan staging database
|
||||
./mysql-scanner --host staging-db.example.com --user dev_user --password *** \
|
||||
--db wordpress_staging --patterns script_tag,iframe_tag,javascript_protocol
|
||||
```
|
||||
|
||||
## WordPress-Specific Tables
|
||||
|
||||
The scanner automatically handles these common WordPress tables:
|
||||
|
||||
- `wp_posts` - post_content, post_excerpt, post_title
|
||||
- `wp_options` - option_value (theme options, plugins settings)
|
||||
- `wp_comments` - comment_content
|
||||
- `wp_commentmeta` - meta_value
|
||||
- `wp_postmeta` - meta_value
|
||||
- `wp_usermeta` - meta_value
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Failed
|
||||
|
||||
```
|
||||
Error: failed to connect to database: dial tcp 127.0.0.1:3306: connect: connection refused
|
||||
```
|
||||
|
||||
- Check that MySQL is running: `systemctl status mysql`
|
||||
- Verify host and port: `--host localhost --port 3306`
|
||||
- Check firewall rules
|
||||
|
||||
### Access Denied
|
||||
|
||||
```
|
||||
Error: Access denied for user 'wp_user'@'localhost'
|
||||
```
|
||||
|
||||
- Verify username and password
|
||||
- Ensure user has SELECT permissions on the database
|
||||
- Check host restrictions (localhost vs %)
|
||||
|
||||
### No Text Columns Found
|
||||
|
||||
Some tables may only have numeric columns and will be skipped. This is normal.
|
||||
|
||||
### Too Many False Positives
|
||||
|
||||
Some patterns like `eval(` may trigger on legitimate content. Use pattern filtering:
|
||||
|
||||
```bash
|
||||
# Exclude false-positive patterns
|
||||
./mysql-scanner --db mydb --patterns base64_decode,script_tag,iframe_tag
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Never commit scan results to version control** - they may contain sensitive data
|
||||
2. **Use read-only database accounts** - Create a dedicated scanner user with SELECT only
|
||||
3. **Secure scan output files** - Set appropriate file permissions (chmod 600)
|
||||
4. **Rotate scanner credentials** - Change passwords regularly
|
||||
5. **Monitor scan logs** - Track who is running scans and when
|
||||
|
||||
## Creating a Read-Only Scanner User
|
||||
|
||||
```sql
|
||||
-- Create a scanner user with minimal permissions
|
||||
CREATE USER 'scanner'@'localhost' IDENTIFIED BY 'strong_password_here';
|
||||
|
||||
-- Grant SELECT on all tables
|
||||
GRANT SELECT ON wordpress.* TO 'scanner'@'localhost';
|
||||
|
||||
-- Flush privileges
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
-- Verify
|
||||
SHOW GRANTS FOR 'scanner'@'localhost';
|
||||
```
|
||||
5
mysql-scanner/go.mod
Normal file
5
mysql-scanner/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module github.com/wltbagent/mysql-scanner
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/go-sql-driver/mysql v1.7.1
|
||||
2
mysql-scanner/go.sum
Normal file
2
mysql-scanner/go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
579
mysql-scanner/main.go
Normal file
579
mysql-scanner/main.go
Normal file
@@ -0,0 +1,579 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
// Config holds scanner configuration
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
Database string
|
||||
Patterns []string
|
||||
JSON bool
|
||||
Output string
|
||||
}
|
||||
|
||||
// Finding represents a detected issue
|
||||
type Finding struct {
|
||||
Table string `json:"table"`
|
||||
Column string `json:"column"`
|
||||
RowID string `json:"row_id"`
|
||||
RiskLevel string `json:"risk_level"`
|
||||
Pattern string `json:"pattern"`
|
||||
Match string `json:"match"`
|
||||
Snippet string `json:"snippet"`
|
||||
LineNumbers []int `json:"line_numbers,omitempty"`
|
||||
}
|
||||
|
||||
// ScanResult holds complete scan results
|
||||
type ScanResult struct {
|
||||
Database string `json:"database"`
|
||||
Tables int `json:"tables_scanned"`
|
||||
Rows int `json:"rows_scanned"`
|
||||
Findings []Finding `json:"findings"`
|
||||
HighRisk int `json:"high_risk_count"`
|
||||
MedRisk int `json:"medium_risk_count"`
|
||||
LowRisk int `json:"low_risk_count"`
|
||||
}
|
||||
|
||||
// Pattern defines a detection pattern
|
||||
type Pattern struct {
|
||||
Name string
|
||||
Risk string
|
||||
Regex *regexp.Regexp
|
||||
Sensitive bool
|
||||
}
|
||||
|
||||
var patterns []Pattern
|
||||
|
||||
func init() {
|
||||
// Initialize detection patterns
|
||||
patterns = []Pattern{
|
||||
// PHP dangerous functions - HIGH RISK
|
||||
{
|
||||
Name: "eval_function",
|
||||
Risk: "high",
|
||||
Regex: regexp.MustCompile(`\beval\s*\(`),
|
||||
},
|
||||
{
|
||||
Name: "assert_function",
|
||||
Risk: "high",
|
||||
Regex: regexp.MustCompile(`\bassert\s*\(`),
|
||||
},
|
||||
{
|
||||
Name: "create_function",
|
||||
Risk: "high",
|
||||
Regex: regexp.MustCompile(`\bcreate_function\s*\(`),
|
||||
},
|
||||
{
|
||||
Name: "exec_function",
|
||||
Risk: "high",
|
||||
Regex: regexp.MustCompile(`\b(?:exec|system|passthru|shell_exec)\s*\(`),
|
||||
},
|
||||
|
||||
// Base64 with decode - HIGH RISK
|
||||
{
|
||||
Name: "base64_decode",
|
||||
Risk: "high",
|
||||
Regex: regexp.MustCompile(`\bbase64_decode\s*\(`),
|
||||
},
|
||||
{
|
||||
Name: "gzinflate",
|
||||
Risk: "high",
|
||||
Regex: regexp.MustCompile(`\bgzinflate\s*\(`),
|
||||
},
|
||||
{
|
||||
Name: "str_rot13",
|
||||
Risk: "medium",
|
||||
Regex: regexp.MustCompile(`\bstr_rot13\s*\(`),
|
||||
},
|
||||
|
||||
// Long base64 strings - MEDIUM RISK
|
||||
{
|
||||
Name: "long_base64",
|
||||
Risk: "medium",
|
||||
Regex: regexp.MustCompile(`[A-Za-z0-9+/]{200,}={0,2}`),
|
||||
},
|
||||
|
||||
// HTML/JS injection - MEDIUM RISK
|
||||
{
|
||||
Name: "script_tag",
|
||||
Risk: "medium",
|
||||
Regex: regexp.MustCompile(`(?i)<script[^>]*>.*?</script>`),
|
||||
},
|
||||
{
|
||||
Name: "iframe_tag",
|
||||
Risk: "medium",
|
||||
Regex: regexp.MustCompile(`(?i)<iframe[^>]*(?:hidden|width=0|height=0)[^>]*>`),
|
||||
},
|
||||
{
|
||||
Name: "javascript_protocol",
|
||||
Risk: "medium",
|
||||
Regex: regexp.MustCompile(`(?i)javascript:\s*[^\s"'>]+`),
|
||||
},
|
||||
{
|
||||
Name: "event_handler",
|
||||
Risk: "medium",
|
||||
Regex: regexp.MustCompile(`(?i)\b(?:onerror|onload|onclick|onmouseover)\s*=`),
|
||||
},
|
||||
|
||||
// Web shell patterns - HIGH RISK
|
||||
{
|
||||
Name: "superglobal_eval",
|
||||
Risk: "high",
|
||||
Regex: regexp.MustCompile(`(?:\$_(?:GET|POST|REQUEST|COOKIE))\[.*\]\s*\(\s*\$`),
|
||||
},
|
||||
{
|
||||
Name: "error_reporting_suppress",
|
||||
Risk: "medium",
|
||||
Regex: regexp.MustCompile(`error_reporting\s*\(\s*0\s*\)`),
|
||||
},
|
||||
|
||||
// Variable function calls - MEDIUM RISK
|
||||
{
|
||||
Name: "variable_function",
|
||||
Risk: "medium",
|
||||
Regex: regexp.MustCompile(`\$[a-zA-Z_]\w*\s*\(`),
|
||||
},
|
||||
|
||||
// Common malware comment signatures - LOW RISK
|
||||
{
|
||||
Name: "malware_comment",
|
||||
Risk: "low",
|
||||
Regex: regexp.MustCompile(`(?i)/\*.*(?:shell|backdoor|webshell|hack|c99|r57).*\*/`),
|
||||
},
|
||||
|
||||
// Obfuscation indicators - MEDIUM RISK
|
||||
{
|
||||
Name: "hex_encode",
|
||||
Risk: "medium",
|
||||
Regex: regexp.MustCompile(`\\x[0-9a-fA-F]{2}`),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
config := parseFlags()
|
||||
|
||||
if config.User == "" || config.Database == "" {
|
||||
log.Fatal("Error: --user and --db are required")
|
||||
}
|
||||
|
||||
// Connect to database
|
||||
db, err := connectDB(config)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
fmt.Printf("Connected to %s@%s:%d/%s\n", config.User, config.Host, config.Port, config.Database)
|
||||
|
||||
// Scan database
|
||||
result, err := scanDatabase(db, config)
|
||||
if err != nil {
|
||||
log.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
|
||||
// Output results
|
||||
if config.JSON {
|
||||
outputJSON(result, config.Output)
|
||||
} else {
|
||||
outputHuman(result)
|
||||
}
|
||||
}
|
||||
|
||||
func parseFlags() Config {
|
||||
host := flag.String("host", getEnv("DB_HOST", "localhost"), "Database host")
|
||||
port := flag.Int("port", getEnvInt("DB_PORT", 3306), "Database port")
|
||||
user := flag.String("user", getEnv("DB_USER", ""), "Database user")
|
||||
password := flag.String("password", getEnv("DB_PASSWORD", ""), "Database password")
|
||||
db := flag.String("db", getEnv("DB_NAME", ""), "Database name")
|
||||
patterns := flag.String("patterns", "", "Comma-separated patterns to scan (default: all)")
|
||||
jsonOut := flag.Bool("json", false, "Output JSON instead of human-readable")
|
||||
output := flag.String("output", "", "Output file path (default: stdout)")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
var patternList []string
|
||||
if *patterns != "" {
|
||||
patternList = strings.Split(*patterns, ",")
|
||||
for i := range patternList {
|
||||
patternList[i] = strings.TrimSpace(patternList[i])
|
||||
}
|
||||
}
|
||||
|
||||
return Config{
|
||||
Host: *host,
|
||||
Port: *port,
|
||||
User: *user,
|
||||
Password: *password,
|
||||
Database: *db,
|
||||
Patterns: patternList,
|
||||
JSON: *jsonOut,
|
||||
Output: *output,
|
||||
}
|
||||
}
|
||||
|
||||
func connectDB(config Config) (*sql.DB, error) {
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true",
|
||||
config.User,
|
||||
config.Password,
|
||||
config.Host,
|
||||
config.Port,
|
||||
config.Database,
|
||||
)
|
||||
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func scanDatabase(db *sql.DB, config Config) (*ScanResult, error) {
|
||||
result := &ScanResult{
|
||||
Database: config.Database,
|
||||
Findings: []Finding{},
|
||||
}
|
||||
|
||||
// Get all tables
|
||||
tables, err := getTables(db, config.Database)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get tables: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d tables\n", len(tables))
|
||||
|
||||
for _, table := range tables {
|
||||
fmt.Printf("Scanning table: %s\n", table)
|
||||
|
||||
columns, err := getTextColumns(db, config.Database, table)
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to get columns for %s: %v", table, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(columns) == 0 {
|
||||
fmt.Printf(" No text columns, skipping\n")
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf(" Scanning %d text columns: %v\n", len(columns), columns)
|
||||
|
||||
rows, err := scanTable(db, table, columns, config.Patterns)
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to scan table %s: %v", table, err)
|
||||
continue
|
||||
}
|
||||
|
||||
result.Findings = append(result.Findings, rows...)
|
||||
result.Tables++
|
||||
result.Rows += len(rows)
|
||||
}
|
||||
|
||||
// Count risk levels
|
||||
for _, f := range result.Findings {
|
||||
switch f.RiskLevel {
|
||||
case "high":
|
||||
result.HighRisk++
|
||||
case "medium":
|
||||
result.MedRisk++
|
||||
case "low":
|
||||
result.LowRisk++
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getTables(db *sql.DB, dbName string) ([]string, error) {
|
||||
query := `SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = ? AND table_type = 'BASE TABLE'`
|
||||
|
||||
rows, err := db.Query(query, dbName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tables []string
|
||||
for rows.Next() {
|
||||
var table string
|
||||
if err := rows.Scan(&table); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tables = append(tables, table)
|
||||
}
|
||||
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func getTextColumns(db *sql.DB, dbName, table string) ([]string, error) {
|
||||
query := `SELECT column_name FROM information_schema.columns
|
||||
WHERE table_schema = ? AND table_name = ?
|
||||
AND data_type IN ('text', 'longtext', 'mediumtext', 'varchar', 'char')
|
||||
ORDER BY ordinal_position`
|
||||
|
||||
rows, err := db.Query(query, dbName, table)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var columns []string
|
||||
for rows.Next() {
|
||||
var col string
|
||||
if err := rows.Scan(&col); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
columns = append(columns, col)
|
||||
}
|
||||
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
func scanTable(db *sql.DB, table string, columns, activePatterns []string) ([]Finding, error) {
|
||||
var findings []Finding
|
||||
|
||||
// Build SELECT query with ID column if exists
|
||||
query, idColumn := buildSelectQuery(table, columns)
|
||||
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Get column names for scanning
|
||||
colNames, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
// Scan row into values
|
||||
values := make([]interface{}, len(colNames))
|
||||
valuePtrs := make([]interface{}, len(colNames))
|
||||
for i := range values {
|
||||
valuePtrs[i] = &values[i]
|
||||
}
|
||||
|
||||
if err := rows.Scan(valuePtrs...); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get row identifier
|
||||
rowID := ""
|
||||
if idColumn != "" {
|
||||
for i, name := range colNames {
|
||||
if name == idColumn && values[i] != nil {
|
||||
rowID = fmt.Sprintf("%v", values[i])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scan each text column
|
||||
for i, name := range colNames {
|
||||
if values[i] == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip ID column
|
||||
if name == idColumn {
|
||||
continue
|
||||
}
|
||||
|
||||
content := fmt.Sprintf("%v", values[i])
|
||||
if len(content) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Run patterns
|
||||
for _, pattern := range patterns {
|
||||
// Filter patterns if specified
|
||||
if len(activePatterns) > 0 && !contains(activePatterns, pattern.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
if pattern.Regex.MatchString(content) {
|
||||
matches := pattern.Regex.FindAllString(content, 10)
|
||||
|
||||
for _, match := range matches {
|
||||
finding := Finding{
|
||||
Table: table,
|
||||
Column: name,
|
||||
RowID: rowID,
|
||||
RiskLevel: pattern.Risk,
|
||||
Pattern: pattern.Name,
|
||||
Match: match,
|
||||
Snippet: truncate(content, 200),
|
||||
}
|
||||
|
||||
findings = append(findings, finding)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return findings, nil
|
||||
}
|
||||
|
||||
func buildSelectQuery(table string, columns []string) (string, string) {
|
||||
// Try to find an ID column for row identification
|
||||
idColumns := []string{"id", "ID", "post_id", "comment_id", "user_id", "option_id", "term_id"}
|
||||
var idColumn string
|
||||
|
||||
// For now, we'll select all columns and figure out ID during scan
|
||||
selectCols := "*"
|
||||
if len(columns) > 0 {
|
||||
// Include ID column if we can guess it
|
||||
for _, idCol := range idColumns {
|
||||
if contains(columns, idCol) {
|
||||
idColumn = idCol
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("SELECT %s FROM %s", selectCols, table)
|
||||
return query, idColumn
|
||||
}
|
||||
|
||||
func outputHuman(result *ScanResult) {
|
||||
fmt.Printf("\n=== SCAN RESULTS ===\n")
|
||||
fmt.Printf("Database: %s\n", result.Database)
|
||||
fmt.Printf("Tables scanned: %d\n", result.Tables)
|
||||
fmt.Printf("Findings: %d total (High: %d, Medium: %d, Low: %d)\n\n",
|
||||
len(result.Findings), result.HighRisk, result.MedRisk, result.LowRisk)
|
||||
|
||||
if len(result.Findings) == 0 {
|
||||
fmt.Println("✓ No suspicious patterns detected")
|
||||
return
|
||||
}
|
||||
|
||||
// Group by risk level
|
||||
high := []Finding{}
|
||||
medium := []Finding{}
|
||||
low := []Finding{}
|
||||
|
||||
for _, f := range result.Findings {
|
||||
switch f.RiskLevel {
|
||||
case "high":
|
||||
high = append(high, f)
|
||||
case "medium":
|
||||
medium = append(medium, f)
|
||||
case "low":
|
||||
low = append(low, f)
|
||||
}
|
||||
}
|
||||
|
||||
// Print high risk first
|
||||
if len(high) > 0 {
|
||||
fmt.Printf("🔴 HIGH RISK (%d)\n", len(high))
|
||||
printFindings(high, 5)
|
||||
}
|
||||
|
||||
if len(medium) > 0 {
|
||||
fmt.Printf("\n🟡 MEDIUM RISK (%d)\n", len(medium))
|
||||
printFindings(medium, 3)
|
||||
}
|
||||
|
||||
if len(low) > 0 {
|
||||
fmt.Printf("\n🟢 LOW RISK (%d)\n", len(low))
|
||||
printFindings(low, 2)
|
||||
}
|
||||
}
|
||||
|
||||
func printFindings(findings []Finding, maxDisplay int) {
|
||||
display := min(maxDisplay, len(findings))
|
||||
for i := 0; i < display; i++ {
|
||||
f := findings[i]
|
||||
fmt.Printf("\n [%s] %s.%s", f.RiskLevel, f.Table, f.Column)
|
||||
if f.RowID != "" {
|
||||
fmt.Printf(" (ID: %s)", f.RowID)
|
||||
}
|
||||
fmt.Printf("\n Pattern: %s\n", f.Pattern)
|
||||
fmt.Printf(" Match: %s\n", truncate(f.Match, 100))
|
||||
fmt.Printf(" Snippet: %s\n", f.Snippet)
|
||||
}
|
||||
|
||||
if len(findings) > maxDisplay {
|
||||
fmt.Printf("\n ... and %d more\n", len(findings)-maxDisplay)
|
||||
}
|
||||
}
|
||||
|
||||
func outputJSON(result *ScanResult, outputPath string) {
|
||||
data, err := json.MarshalIndent(result, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to marshal JSON: %v", err)
|
||||
}
|
||||
|
||||
if outputPath != "" {
|
||||
if err := os.WriteFile(outputPath, data, 0644); err != nil {
|
||||
log.Fatalf("Failed to write output file: %v", err)
|
||||
}
|
||||
fmt.Printf("Results written to %s\n", outputPath)
|
||||
} else {
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func getEnvInt(key string, fallback int) int {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
var i int
|
||||
fmt.Sscanf(value, "%d", &i)
|
||||
return i
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if strings.EqualFold(s, item) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func truncate(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "..."
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
BIN
mysql-scanner/mysql-scanner
Executable file
BIN
mysql-scanner/mysql-scanner
Executable file
Binary file not shown.
Reference in New Issue
Block a user