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
18 changes: 18 additions & 0 deletions backend/models/FederationMember.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const mongoose = require('mongoose');

const FederationMemberSchema = new mongoose.Schema({
name: { type: String, required: true },
apiUrl: { type: String, required: true },
apiKey: { type: String, required: true },
status: {
type: String,
enum: ['active', 'inactive', 'pending'],
default: 'pending'
},
joinedAt: { type: Date, default: Date.now },
lastSync: { type: Date },
threatCount: { type: Number, default: 0 },
trustScore: { type: Number, default: 50, min: 0, max: 100 }
});

module.exports = mongoose.model('FederationMember', FederationMemberSchema);
12 changes: 11 additions & 1 deletion backend/models/SenderReputation.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,31 @@ const SenderReputationSchema = new mongoose.Schema({
score: { type: Number, default: 50, min: 0, max: 100 },
spamReports: { type: Number, default: 0 },
hamReports: { type: Number, default: 0 },
lastSeen: { type: Date, default: Date.now }
totalReports: { type: Number, default: 0 },
lastSeen: { type: Date, default: Date.now },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});

// ✅ Record ham (increase score)
SenderReputationSchema.methods.recordHam = function() {
this.hamReports += 1;
this.totalReports += 1;
this.score = Math.min(100, this.score + 2);
this.updatedAt = new Date();
return this.save();
};

// ✅ Record spam (decrease score)
SenderReputationSchema.methods.recordSpam = function() {
this.spamReports += 1;
this.totalReports += 1;
this.score = Math.max(0, this.score - 5);
this.updatedAt = new Date();
return this.save();
};

// ✅ Get reputation level
SenderReputationSchema.methods.getLevel = function() {
if (this.score >= 70) return 'trusted';
if (this.score >= 40) return 'neutral';
Expand Down
21 changes: 21 additions & 0 deletions backend/routes/historyRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

router.get('/recent',protect, async(req,res)=> {
try{
const predictions= await Prediction.find({userId: req.user.id })

Check failure on line 37 in backend/routes/historyRoutes.js

View workflow job for this annotation

GitHub Actions / Node backend (npm test)

'Prediction' is not defined
.sort({ createdAt: -1 })
.limit(10)
.select('text result createdAt');
Expand All @@ -45,6 +45,27 @@
}
});

const getStatsSummary = async (req, res) => {
try {
const stats = await History.aggregate([
{ $match: { user: req.user.id } },
{
$group: {
_id: {
date: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } },
prediction: "$prediction"
},
count: { $sum: 1 }
}
},
{ $sort: { "_id.date": 1 } }
]);
res.json(stats);
} catch (err) {
res.status(500).json({ error: "Failed to compile statistics" });
}
};

router.get('/',protect,async(req,res) => {
try{
const{startDate, endDate, limit =50 } =req.query;
Expand Down
71 changes: 71 additions & 0 deletions backend/routes/predictionRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const { classifyMlApiError } = require('../utils/errorHelper');
const validationMessages = require('../utils/validationMessages');
const History = require('../models/History');
const Rule = require('../models/Rule');
const User = require('../models/User');
const SenderReputation = require('../models/SenderReputation');
const { matchKeywordRule } = require('../utils/keywordRules');
const { evaluateAdminRules } = require('../utils/adminRuleEvaluator');
const upload = multer();
Expand Down Expand Up @@ -342,6 +344,75 @@ router.post("/predict", predictLimiter, preventCacheStampede, protect, checkCach
}
});

router.post('/predict', protect, async (req, res) => {
try {
const { text, sender } = req.body;

// Get ML prediction
const mlResult = await getMLPrediction(text);

// Check sender reputation
let reputationScore = 50;
let reputationLevel = 'neutral';

if (sender) {
const domain = sender.split('@')[1];
let rep = await SenderReputation.findOne({ domain });

if (rep) {
reputationScore = rep.score;
reputationLevel = rep.getLevel();

// If sender is malicious, return spam immediately
if (reputationLevel === 'suspicious' && reputationScore < 30) {
return res.json({
prediction: 'spam',
confidence: 0.95,
reason: 'Sender has low reputation score',
senderReputation: {
score: reputationScore,
level: reputationLevel
}
});
}

// If sender is trusted, reduce spam confidence
if (reputationLevel === 'trusted' && mlResult.prediction === 'spam') {
return res.json({
prediction: 'ham',
confidence: 0.70,
reason: 'Trusted sender',
senderReputation: {
score: reputationScore,
level: reputationLevel
}
});
}
}
}

// Adjust confidence based on reputation
let adjustment = 0;
if (reputationLevel === 'trusted') adjustment = -0.15;
if (reputationLevel === 'suspicious') adjustment = 0.20;

let finalConfidence = mlResult.confidence + adjustment;
finalConfidence = Math.max(0, Math.min(1, finalConfidence));

res.json({
prediction: finalConfidence > 0.5 ? 'spam' : 'ham',
confidence: finalConfidence,
mlConfidence: mlResult.confidence,
senderReputation: {
score: reputationScore,
level: reputationLevel
}
});
} catch (error) {
res.status(500).json({ error: 'Prediction failed' });
}
});

router.post("/feedback", protect, async (req, res) => {
try {
const { text, predicted_label, correct_label, historyId, note } = req.body;
Expand Down
2 changes: 2 additions & 0 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const predictionRoutes = require("./routes/predictionRoutes");
const feedbackRoutes = require('./routes/feedbackRoutes');
const emailIntegrationRoutes = require("./routes/emailIntegrationRoutes");
const imapRoutes = require("./routes/imapRoutes");
const federationRoutes = require('./routes/federationRoutes');
const utilityRoutes = require("./routes/utilityRoutes");
const bulkPredictRoutes = require("./routes/bulkPredict");

Expand Down Expand Up @@ -71,6 +72,7 @@ const app = express();
const { apiLimiter } = require('./middleware/rateLimiter');
app.use('/predict', apiLimiter);
app.use('/api', feedbackRoutes);
app.use('/api/federation', federationRoutes);

// Trust the first proxy so express-rate-limit correctly identifies user IPs

Expand Down
67 changes: 67 additions & 0 deletions backend/services/patchAlgorithm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const crypto = require('crypto');

class PATCHAlgorithm {
/**
* PATCH (Privacy-Preserving Anonymization for Collaborative Threat Handling)
* Ensures full anonymity of exchanged content
*/

anonymize(email) {
// 1. Normalize email (lowercase, remove special chars)
const normalized = this.normalize(email);

// 2. Generate hash (SHA-256)
const hash = crypto.createHash('sha256').update(normalized).digest('hex');

// 3. Extract patterns (n-grams)
const patterns = this.extractPatterns(normalized);

// 4. Apply differential privacy (add noise)
const anonymized = this.addNoise(patterns);

return {
hash,
patterns: anonymized,
timestamp: new Date().toISOString()
};
}

normalize(text) {
return text
.toLowerCase()
.replace(/[^a-zA-Z0-9\s]/g, '')
.trim();
}

extractPatterns(text) {
const words = text.split(/\s+/);
const patterns = [];

// Single words
words.forEach(w => {
if (w.length > 3) patterns.push(w);
});

// Bigrams
for (let i = 0; i < words.length - 1; i++) {
patterns.push(`${words[i]} ${words[i+1]}`);
}

return patterns;
}

addNoise(patterns) {
// Randomly sample 70% of patterns to add noise
const sampleSize = Math.floor(patterns.length * 0.7);
const shuffled = patterns.sort(() => 0.5 - Math.random());
return shuffled.slice(0, sampleSize);
}

compare(pattern1, pattern2) {
// Calculate similarity score
const common = pattern1.filter(p => pattern2.includes(p));
return common.length / Math.max(pattern1.length, pattern2.length);
}
}

module.exports = new PATCHAlgorithm();
14 changes: 13 additions & 1 deletion frontend/src/components/SpamInsightsDashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import api from "../utils/axiosInstance";
import { SpamTrends } from './SpamTrends';
import { RecentActivity } from './RecentActivity';
import { RateLimitDashboard } from './RateLimitDashboard';
import { AccuracyMeter } from './AccuracyMeter';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';


export default function SpamInsightsDashboard() {
Expand Down Expand Up @@ -32,7 +33,7 @@

useEffect(() => {
fetchInsights();
}, []);

Check warning on line 36 in frontend/src/components/SpamInsightsDashboard.jsx

View workflow job for this annotation

GitHub Actions / Frontend (npm test)

React Hook useEffect has a missing dependency: 'fetchInsights'. Either include it or remove the dependency array

const handleCategoryChange = (e) => {
const val = e.target.value;
Expand Down Expand Up @@ -157,6 +158,17 @@
</div>
</div>

<ResponsiveContainer width="100%" height={300}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="spam" stroke="#ef4444" strokeWidth={2} />
<Line type="monotone" dataKey="ham" stroke="#22c55e" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>

{/* Trending Phrases */}
<div>
<h3 className="text-[10px] font-extrabold uppercase tracking-wider mb-2 opacity-70">Trending Phrases</h3>
Expand Down
Loading