Skip to content
Merged
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
2 changes: 1 addition & 1 deletion backend/routes/saltingRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const { checkPermission } = require('../middleware/zeroTrust');
const { spawn } = require('child_process');
const path = require('path');

const SALTING_SCRIPT = path.join(__dirname, '../text_salting_detector.js');
const SALTING_SCRIPT = path.join(__dirname, '../text_salting_detector.py');

router.post('/detect', protect, async (req, res) => {
try {
Expand Down
2 changes: 2 additions & 0 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const { preventCacheStampede } = require('./middleware/cacheMiddleware');
const adversarialRoutes = require('./routes/adversarialRoutes');
const evoMailRoutes = require('./routes/evoMailRoutes');
const poisoningRoutes = require('./routes/poisoningRoutes');
const saltingRoutes = require('./routes/saltingRoutes');

const healthRoutes = require("./routes/healthRoutes");
const predictionRoutes = require("./routes/predictionRoutes");
Expand Down Expand Up @@ -287,6 +288,7 @@ app.use("/api/reports", reportRoutes);
app.use('/api/adversarial', adversarialRoutes);
app.use('/api/evomail', evoMailRoutes);
app.use('/api/poisoning', poisoningRoutes);
app.use('/api/salting', saltingRoutes);



Expand Down
78 changes: 73 additions & 5 deletions backend/text_salting_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
"""

import re
import sys
import json
import base64
import argparse
from pathlib import Path
from datetime import datetime
import hashlib
Expand Down Expand Up @@ -186,7 +188,7 @@ def render(self, html):
return img

except Exception as e:
print(f"⚠️ HTML rendering failed: {e}")
print(f"⚠️ HTML rendering failed: {e}", file=sys.stderr)
# Fallback: Create image with text
return self._render_text_fallback(html)

Expand Down Expand Up @@ -245,7 +247,7 @@ def extract(self, image):

return text.strip()
except Exception as e:
print(f"⚠️ OCR failed: {e}")
print(f"⚠️ OCR failed: {e}", file=sys.stderr)
return ""


Expand Down Expand Up @@ -310,9 +312,10 @@ def detect(self, html):
results['recommendations'].append('Email appears to be mostly hidden text - potential salting attack')

elif hidden_chars > visible_chars * self.threshold_ratio:
ratio = hidden_chars / (visible_chars + 1)
results['is_suspicious'] = True
results['confidence'] = min(0.95, hidden_chars / (visible_chars + 1))
results['analysis']['reason'] = f'Hidden text ({hidden_chars} chars) exceeds visible text ({visible_chars} chars) by {hidden_chars/visible_chars:.1f}x'
results['confidence'] = min(0.95, ratio)
results['analysis']['reason'] = f'Hidden text ({hidden_chars} chars) exceeds visible text ({visible_chars} chars) by {ratio:.1f}x'
results['recommendations'].append('Significant text salting detected - hidden content used to dilute spam signals')

elif hidden_chars > 0 and hidden_chars < visible_chars * 0.5:
Expand Down Expand Up @@ -508,5 +511,70 @@ def main():
return detector


def _command_detect(detector, params):
html = params.get("html")
if not isinstance(html, str) or not html.strip():
raise ValueError("Parameter 'html' is required and must be a non-empty string")
return detector.detect(html)


def _command_status(_detector):
return {
"ready": True,
"outputDir": str(OUTPUT_DIR),
}


def _emit(payload):
"""Write a single JSON object to stdout for the calling Express process.
All diagnostics (render/OCR warnings) go to stderr instead of print()'s
default stdout so this is always the only thing on stdout."""
sys.stdout.write(json.dumps(payload, default=str))
sys.stdout.flush()


def run_cli(argv=None):
parser = argparse.ArgumentParser(description="Text Salting Defense CLI")
parser.add_argument(
"--command",
choices=["detect", "status"],
help="Operation to run. Omit to run the interactive demo.",
)
parser.add_argument(
"--params",
default="{}",
help="JSON-encoded parameters for the command.",
)
args = parser.parse_args(argv)

# No command -> preserve the original demo behaviour when run directly.
if args.command is None:
main()
return 0

try:
params = json.loads(args.params)
except json.JSONDecodeError as error:
_emit({"success": False, "command": args.command, "error": f"Invalid --params JSON: {error}"})
return 1
if not isinstance(params, dict):
_emit({"success": False, "command": args.command, "error": "--params must be a JSON object"})
return 1

try:
detector = TextSaltingDetector()
if args.command == "detect":
result = _command_detect(detector, params)
else:
result = _command_status(detector)
except Exception as error: # surfaced to Express via stderr + non-zero exit
print(f"{args.command} failed: {error}", file=sys.stderr)
_emit({"success": False, "command": args.command, "error": str(error)})
return 1

_emit({"success": True, "command": args.command, **result})
return 0


if __name__ == "__main__":
detector = main()
sys.exit(run_cli())
Loading