SaaS Account Takeover Detection (AutoML)
Tested against SynapCores CE v1.7.0.1-ce (the currently-shipped release on Docker Hub:
synapcores/community:v1.7.0.1-ce). Copy each block in order — the numbers in the comments come from this run.
Objective
Catch credential-stuffing and session-hijack attempts the moment a login event hits your auth service. The model learns the joint distribution of recent failed attempts, geographic jump from the previous successful login, user-agent change, and hour of day.
Why this matters: Verizon's DBIR puts stolen credentials at the #1 root
cause of breaches for 6+ years running. Static rules (if failed > 5 block) leak both ways — they false-positive on travelling executives and
under-fire on slow credential-stuffing campaigns that pace themselves
under threshold. A model learns the combination.
Step 1 — Schema + labelled login history
180 logins: 158 normal + 22 confirmed takeovers (~12% takeover rate).
DROP TABLE IF EXISTS sat_login;
CREATE TABLE sat_login (
id INTEGER PRIMARY KEY,
failed_5m INTEGER, -- failed attempts on this account in last 5 min
geo_jump_km DOUBLE, -- distance from previous successful login city
ua_change INTEGER, -- 0/1 — user-agent differs from prior session
hour INTEGER, -- hour of day (UTC)
is_takeover INTEGER
);
INSERT INTO sat_login VALUES
(25,1,0.3,0,15,0),
(1,0,16.8,0,15,0),
(112,1,24.6,0,18,0),
(75,0,28.7,0,9,0),
(81,0,20.0,0,11,0),
(76,0,18.4,0,15,0),
(93,1,17.1,0,12,0),
(145,1,25.6,0,17,0),
(59,1,28.9,0,8,0),
(11,0,25.3,0,16,0),
(43,0,28.5,0,15,0),
(74,0,18.0,0,15,0),
(18,0,28.7,0,18,0),
(161,30,7361.9,1,2,1),
(134,0,2.8,0,10,0),
(63,0,28.1,0,19,0),
(45,0,15.4,0,12,0),
(108,1,27.3,0,14,0),
(124,2,14.8,0,19,0),
(159,33,5321.5,1,23,1),
(96,1,8.0,0,10,0),
(5,0,2.7,0,19,0),
(155,2,4.0,0,11,0),
(12,0,8.8,0,8,0),
(119,0,0.9,0,9,0),
(99,1,11.1,0,9,0),
(163,29,4420.1,1,22,1),
(123,0,20.3,0,15,0),
(125,0,6.6,0,16,0),
(149,0,25.9,0,16,0),
(15,1,9.5,0,11,0),
(122,0,30.0,0,19,0),
(9,0,1.1,0,11,0),
(157,0,25.0,0,14,0),
(24,1,3.5,0,11,0),
(42,0,18.9,0,16,0),
(84,2,6.3,0,10,0),
(151,1,25.7,0,11,0),
(23,0,0.5,0,14,0),
(164,20,6580.5,1,3,1),
(14,0,16.5,0,9,0),
(80,2,14.9,0,11,0),
(91,0,2.2,0,8,0),
(27,0,15.3,0,19,0),
(106,0,28.0,0,19,0),
(152,2,18.8,0,13,0),
(94,1,16.9,0,18,0),
(46,0,21.1,0,12,0),
(39,0,17.2,0,14,0),
(58,0,0.5,0,13,0),
(65,0,20.6,0,9,0),
(170,11,1768.0,1,22,1),
(113,2,3.7,0,11,0),
(162,15,5873.9,1,22,1),
(117,0,2.5,0,15,0),
(126,0,24.9,0,16,0),
(143,0,21.7,0,11,0),
(16,0,0.9,0,17,0),
(49,2,3.8,0,15,0),
(17,1,12.0,0,12,0),
(141,1,29.3,0,8,0),
(171,37,1358.8,1,0,1),
(147,2,0.7,0,18,0),
(29,0,12.6,0,11,0),
(115,0,23.7,0,13,0),
(69,1,2.4,0,18,0),
(129,0,25.5,0,16,0),
(144,1,3.1,0,19,0),
(174,31,895.4,1,22,1),
(7,2,19.6,0,17,0),
(101,0,21.4,0,15,0),
(165,14,975.0,1,23,1),
(140,1,27.8,0,10,0),
(168,21,2041.9,1,1,1),
(32,0,29.9,0,17,0),
(2,0,25.7,0,11,0),
(4,1,5.6,0,15,0),
(38,1,3.8,0,17,0),
(110,1,28.3,0,15,0),
(176,34,4068.4,1,22,1),
(172,28,3614.4,1,3,1),
(62,0,16.4,0,19,0),
(90,0,25.2,0,17,0),
(89,0,15.6,0,18,0),
(148,0,15.9,0,10,0),
(40,0,4.6,0,11,0),
(169,26,7473.6,1,0,1),
(52,1,2.1,0,14,0),
(68,1,11.4,0,8,0),
(37,0,18.0,0,13,0),
(118,0,12.6,0,12,0),
(105,0,23.8,0,11,0),
(142,0,6.8,0,18,0),
(8,1,18.7,0,16,0),
(107,0,13.5,0,11,0),
(79,0,27.2,0,18,0),
(120,0,1.1,0,16,0),
(64,1,23.8,0,13,0),
(22,1,8.1,0,9,0),
(136,2,24.0,0,14,0),
(133,0,12.0,0,17,0),
(21,0,17.0,0,11,0),
(50,0,25.6,0,17,0),
(139,0,3.3,0,14,0),
(98,0,18.9,0,18,0),
(86,1,28.5,0,12,0),
(153,2,9.5,0,13,0),
(160,9,1491.6,1,4,1),
(10,1,23.3,0,13,0),
(83,2,11.5,0,17,0),
(179,8,3130.6,1,0,1),
(57,0,28.4,0,17,0),
(82,1,24.1,0,11,0),
(127,1,15.7,0,16,0),
(109,0,19.6,0,16,0),
(73,1,2.5,0,16,0),
(177,31,3718.2,1,1,1),
(54,0,15.9,0,10,0),
(135,0,16.2,0,14,0),
(154,2,5.8,0,11,0),
(71,1,11.7,0,19,0),
(44,1,12.6,0,9,0),
(116,0,21.5,0,8,0),
(36,0,6.3,0,18,0),
(33,1,3.0,0,10,0),
(34,0,7.7,0,17,0),
(131,0,3.4,0,8,0),
(158,0,21.1,0,11,0),
(97,1,29.5,0,9,0),
(51,0,6.4,0,11,0),
(156,0,2.6,0,14,0),
(180,33,4901.7,1,1,1),
(103,0,10.4,0,19,0),
(114,2,29.4,0,12,0),
(66,2,12.5,0,8,0),
(61,1,9.6,0,9,0),
(28,1,12.6,0,14,0),
(175,10,6878.7,1,1,1),
(6,1,17.9,0,14,0),
(102,0,1.1,0,11,0),
(31,0,26.5,0,11,0),
(55,0,4.2,0,15,0),
(128,0,20.1,0,17,0),
(132,1,0.8,0,8,0),
(100,1,6.0,0,15,0),
(20,0,29.4,0,14,0),
(137,0,14.2,0,10,0),
(167,17,2164.9,1,0,1),
(146,0,5.9,0,16,0),
(166,40,7316.7,1,3,1),
(56,2,19.7,0,19,0),
(87,1,3.4,0,15,0),
(77,0,0.3,0,12,0),
(70,0,7.7,0,14,0),
(60,1,2.2,0,9,0),
(13,1,13.7,0,12,0),
(92,0,18.2,0,18,0),
(3,0,15.4,0,18,0),
(26,1,5.3,0,16,0),
(121,0,23.6,0,10,0),
(78,0,18.4,0,16,0),
(178,18,1482.6,1,23,1),
(19,0,6.3,0,8,0),
(48,0,5.3,0,19,0),
(53,0,1.8,0,10,0),
(173,40,3352.9,1,1,1),
(47,0,27.1,0,15,0),
(111,0,29.5,0,9,0),
(95,0,13.6,0,8,0),
(138,0,27.0,0,15,0),
(85,0,25.1,0,8,0),
(35,0,8.9,0,9,0),
(104,2,15.0,0,10,0),
(41,1,7.5,0,11,0),
(30,0,25.9,0,17,0),
(72,1,13.7,0,15,0),
(67,2,17.2,0,17,0),
(88,0,6.4,0,14,0),
(150,1,15.9,0,16,0),
(130,0,17.2,0,11,0)
;
SELECT COUNT(*) AS total, SUM(is_takeover) AS confirmed_ato FROM sat_login;
-- → 180 rows, 22 confirmed ATO
Step 2 — Train + deploy the classifier
CREATE EXPERIMENT sat_clf AS
SELECT failed_5m, geo_jump_km, ua_change, hour, is_takeover AS target
FROM sat_login
WITH (
task_type = 'binary_classification',
target_column = 'target',
optimization_metric = 'auc',
max_trials = 8,
time_budget_seconds = 120,
algorithms = ['logistic_regression', 'random_forest', 'gradient_boosting'],
validation_strategy = 'kfold',
n_folds = 3,
feature_engineering = false,
hyperparameter_strategy = 'random'
);
DEPLOY MODEL sat_pred FROM EXPERIMENT sat_clf;
-- best_score = 1.0 (cleanly separable on these features)
Step 3 — Score live login events
-- A normal mid-morning login, same UA, no geo jump
SELECT AUTOML.PREDICT('sat_pred', 1, 5.0, 0, 11) AS risk;
-- → 0.063 (LOW)
-- 28 failed attempts in 5 min, 5500 km jump, new UA, 3am
SELECT AUTOML.PREDICT('sat_pred', 28, 5500.0, 1, 3) AS risk;
-- → 0.958 (BLOCK + step-up MFA)
Step 4 — Sweep the entire log
SELECT id, failed_5m, geo_jump_km, ua_change, hour,
is_takeover AS actual,
AUTOML.PREDICT('sat_pred', failed_5m, geo_jump_km, ua_change, hour) AS risk
FROM sat_login
ORDER BY risk DESC
LIMIT 10;
Productionizing
- Stream every login event into
sat_login(Kafka → AIDB ingest). - Retrain weekly on the rolling 30-day labelled window — the threat landscape evolves.
- Wire
risk > 0.7→ step-up MFA;risk > 0.9→ block + page IR. - The 4 features above generalize to every SaaS auth stack; add IP-ASN reputation + impossible-velocity if you have them.