SaaS Account Takeover Detection (AutoML)

Train a binary classifier on logins to catch credential-stuffing + session hijack attempts in real time — the SaaS security ground-truth pattern for account takeover (ATO).

All recipes· anomaly-detection· 5 minutesintermediatesql
Instance: localhost:8080

Opens your running SynapCores (SaaS Account Takeover Detection (AutoML) will be staged for a preview — nothing runs until you click Run). No instance yet? Install free in ~30s.

Share

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

  1. Stream every login event into sat_login (Kafka → AIDB ingest).
  2. Retrain weekly on the rolling 30-day labelled window — the threat landscape evolves.
  3. Wire risk > 0.7 → step-up MFA; risk > 0.9 → block + page IR.
  4. The 4 features above generalize to every SaaS auth stack; add IP-ASN reputation + impossible-velocity if you have them.

Get SynapCores Community Edition →

Tags

anomaly-detectionautomlsecuritysaasaccount-takeoverclassification

Run this on your own machine

Install SynapCores Community Edition free, paste the SQL or Cypher above into the bundled web UI, and watch it run.

Download Free CE