audit-device-tracker 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +280 -0
- package/dist/adapters/geo/maxmind.d.ts +31 -0
- package/dist/adapters/geo/maxmind.js +25 -0
- package/dist/adapters/geo/static.d.ts +7 -0
- package/dist/adapters/geo/static.js +10 -0
- package/dist/adapters/storage/memory.d.ts +14 -0
- package/dist/adapters/storage/memory.js +44 -0
- package/dist/adapters/storage/sqlite.d.ts +30 -0
- package/dist/adapters/storage/sqlite.js +120 -0
- package/dist/client/fingerprint.d.ts +36 -0
- package/dist/client/fingerprint.js +37 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/core/geo-distance.d.ts +5 -0
- package/dist/core/geo-distance.js +10 -0
- package/dist/core/parser.d.ts +7 -0
- package/dist/core/parser.js +78 -0
- package/dist/core/rules/impossible-travel.d.ts +12 -0
- package/dist/core/rules/impossible-travel.js +30 -0
- package/dist/core/rules/index.d.ts +11 -0
- package/dist/core/rules/index.js +25 -0
- package/dist/core/rules/new-device.d.ts +3 -0
- package/dist/core/rules/new-device.js +16 -0
- package/dist/core/rules/new-ip.d.ts +3 -0
- package/dist/core/rules/new-ip.js +9 -0
- package/dist/core/rules/shared-device.d.ts +6 -0
- package/dist/core/rules/shared-device.js +16 -0
- package/dist/core/rules/simultaneous-login.d.ts +7 -0
- package/dist/core/rules/simultaneous-login.js +22 -0
- package/dist/core/scoring.d.ts +10 -0
- package/dist/core/scoring.js +14 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +9 -0
- package/dist/middleware/express.d.ts +12 -0
- package/dist/middleware/express.js +11 -0
- package/dist/tracker.d.ts +23 -0
- package/dist/tracker.js +157 -0
- package/dist/types.d.ts +141 -0
- package/dist/types.js +1 -0
- package/package.json +42 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
export type DeviceType = 'desktop' | 'mobile' | 'tablet' | 'bot' | 'unknown';
|
|
2
|
+
export type IdSource = 'cookie' | 'fingerprint';
|
|
3
|
+
export type RiskLevel = 'low' | 'medium' | 'high';
|
|
4
|
+
export type BuiltInRiskReason = 'first_device' | 'new_device' | 'device_shared_with_other_users' | 'new_ip' | 'impossible_travel' | 'simultaneous_login';
|
|
5
|
+
/** A built-in reason, or any string produced by your own rules. */
|
|
6
|
+
export type RiskReason = BuiltInRiskReason | (string & {});
|
|
7
|
+
/** Minimal request shape: compatible with Express, Fastify, Node http. */
|
|
8
|
+
export interface RequestLike {
|
|
9
|
+
headers: Record<string, string | string[] | undefined>;
|
|
10
|
+
ip?: string;
|
|
11
|
+
socket?: {
|
|
12
|
+
remoteAddress?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Where an IP address is located. */
|
|
16
|
+
export interface GeoLocation {
|
|
17
|
+
/** ISO country code, e.g. "CI". */
|
|
18
|
+
country: string | null;
|
|
19
|
+
city: string | null;
|
|
20
|
+
latitude: number;
|
|
21
|
+
longitude: number;
|
|
22
|
+
}
|
|
23
|
+
/** Anything able to turn an IP address into a location (MaxMind, a web API, a test table...). */
|
|
24
|
+
export interface GeoProvider {
|
|
25
|
+
lookup(ip: string): GeoLocation | null | Promise<GeoLocation | null>;
|
|
26
|
+
}
|
|
27
|
+
/** Everything we extract from one incoming request. */
|
|
28
|
+
export interface RequestContext {
|
|
29
|
+
ip: string;
|
|
30
|
+
userAgent: string;
|
|
31
|
+
language: string | null;
|
|
32
|
+
browser: string;
|
|
33
|
+
browserVersion: string;
|
|
34
|
+
os: string;
|
|
35
|
+
osVersion: string;
|
|
36
|
+
deviceType: DeviceType;
|
|
37
|
+
cookieDeviceId: string | null;
|
|
38
|
+
/** Browser-side fingerprint sent by the optional client script, if well formed. */
|
|
39
|
+
clientFingerprint: string | null;
|
|
40
|
+
fingerprint: string;
|
|
41
|
+
}
|
|
42
|
+
/** One device, as stored for one user. */
|
|
43
|
+
export interface DeviceRecord {
|
|
44
|
+
id: string;
|
|
45
|
+
userId: string;
|
|
46
|
+
fingerprint: string;
|
|
47
|
+
idSource: IdSource;
|
|
48
|
+
browser: string;
|
|
49
|
+
browserVersion: string;
|
|
50
|
+
os: string;
|
|
51
|
+
osVersion: string;
|
|
52
|
+
deviceType: DeviceType;
|
|
53
|
+
language: string | null;
|
|
54
|
+
firstSeen: Date;
|
|
55
|
+
lastSeen: Date;
|
|
56
|
+
loginCount: number;
|
|
57
|
+
lastIp: string;
|
|
58
|
+
knownIps: string[];
|
|
59
|
+
trusted: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** One login attempt, kept for the audit trail. */
|
|
62
|
+
export interface LoginEvent {
|
|
63
|
+
userId: string;
|
|
64
|
+
deviceId: string;
|
|
65
|
+
ip: string;
|
|
66
|
+
userAgent: string;
|
|
67
|
+
at: Date;
|
|
68
|
+
riskScore: number;
|
|
69
|
+
reasons: RiskReason[];
|
|
70
|
+
location: GeoLocation | null;
|
|
71
|
+
}
|
|
72
|
+
/** What one rule found: why it is suspicious and how many points it adds. */
|
|
73
|
+
export interface Finding {
|
|
74
|
+
reason: RiskReason;
|
|
75
|
+
points: number;
|
|
76
|
+
detail?: string;
|
|
77
|
+
}
|
|
78
|
+
/** Everything a rule may look at to judge one login. */
|
|
79
|
+
export interface RuleContext {
|
|
80
|
+
userId: string;
|
|
81
|
+
now: Date;
|
|
82
|
+
ip: string;
|
|
83
|
+
device: DeviceRecord;
|
|
84
|
+
isNewDevice: boolean;
|
|
85
|
+
userHadDevices: boolean;
|
|
86
|
+
isNewIp: boolean;
|
|
87
|
+
sharedWithUsers: string[];
|
|
88
|
+
location: GeoLocation | null;
|
|
89
|
+
/** The user's most recent previous login, if any. */
|
|
90
|
+
previousLogin: LoginEvent | null;
|
|
91
|
+
}
|
|
92
|
+
/** A rule inspects a login and returns a finding, or null when nothing is wrong. */
|
|
93
|
+
export interface Rule {
|
|
94
|
+
name: string;
|
|
95
|
+
evaluate(ctx: RuleContext): Finding | null;
|
|
96
|
+
}
|
|
97
|
+
/** What track() returns to your application. */
|
|
98
|
+
export interface TrackResult {
|
|
99
|
+
device: DeviceRecord;
|
|
100
|
+
isNewDevice: boolean;
|
|
101
|
+
riskScore: number;
|
|
102
|
+
riskLevel: RiskLevel;
|
|
103
|
+
reasons: RiskReason[];
|
|
104
|
+
/** The same reasons with their points and a human-readable detail. */
|
|
105
|
+
findings: Finding[];
|
|
106
|
+
location: GeoLocation | null;
|
|
107
|
+
sharedWithUsers: string[];
|
|
108
|
+
deviceCookie: string;
|
|
109
|
+
}
|
|
110
|
+
/** The contract any storage (memory, SQLite, Postgres, Redis...) must respect. */
|
|
111
|
+
export interface StorageAdapter {
|
|
112
|
+
getDevice(userId: string, deviceId: string): Promise<DeviceRecord | null>;
|
|
113
|
+
findDeviceByFingerprint(userId: string, fingerprint: string): Promise<DeviceRecord | null>;
|
|
114
|
+
saveDevice(device: DeviceRecord): Promise<void>;
|
|
115
|
+
listDevices(userId: string): Promise<DeviceRecord[]>;
|
|
116
|
+
listUsersByDevice(deviceId: string): Promise<string[]>;
|
|
117
|
+
removeDevice(userId: string, deviceId: string): Promise<void>;
|
|
118
|
+
addLogin(event: LoginEvent): Promise<void>;
|
|
119
|
+
listLogins(userId: string, limit?: number): Promise<LoginEvent[]>;
|
|
120
|
+
}
|
|
121
|
+
export interface DeviceTrackerOptions {
|
|
122
|
+
storage: StorageAdapter;
|
|
123
|
+
/** Signs the device cookie. At least 16 characters. */
|
|
124
|
+
secret: string;
|
|
125
|
+
cookieName?: string;
|
|
126
|
+
/** Set to false only for local development over plain http. Default: true. */
|
|
127
|
+
cookieSecure?: boolean;
|
|
128
|
+
ipAnonymization?: 'none' | 'partial';
|
|
129
|
+
/** Turns an IP into a location. Without it, location-based rules stay silent. */
|
|
130
|
+
geo?: GeoProvider;
|
|
131
|
+
/** Replaces the built-in rules. Compose with defaultRules(): [...defaultRules(), myRule]. */
|
|
132
|
+
rules?: Rule[];
|
|
133
|
+
/** Header carrying the browser-side fingerprint. Default: x-adt-fp. */
|
|
134
|
+
clientFingerprintHeader?: string;
|
|
135
|
+
/** Risk score at which onSuspicious fires. Default: 60. */
|
|
136
|
+
suspiciousThreshold?: number;
|
|
137
|
+
onNewDevice?: (result: TrackResult, userId: string) => void | Promise<void>;
|
|
138
|
+
onSuspicious?: (result: TrackResult, userId: string) => void | Promise<void>;
|
|
139
|
+
/** Called when a hook, a rule or the geo provider throws. The login is never blocked. */
|
|
140
|
+
onError?: (error: unknown) => void;
|
|
141
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "audit-device-tracker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Identify and audit the devices users log in with. Detect shared credentials, new devices, impossible travel and simultaneous logins.",
|
|
5
|
+
"author": "ADIKO ELIE",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./client": {
|
|
15
|
+
"types": "./dist/client/fingerprint.d.ts",
|
|
16
|
+
"import": "./dist/client/fingerprint.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.build.json",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
|
25
|
+
"demo": "tsc --ignoreConfig examples/demo.ts --outDir .demo --rootDir . --module nodenext --moduleResolution nodenext --target es2022 --types node --skipLibCheck && node --no-warnings .demo/examples/demo.js"
|
|
26
|
+
},
|
|
27
|
+
"keywords": ["security", "audit", "device", "fingerprint", "login", "anomaly-detection", "impossible-travel"],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": { "type": "git", "url": "git+https://github.com/eliedvp/audit-device-tracker.git" },
|
|
30
|
+
"bugs": { "url": "https://github.com/eliedvp/audit-device-tracker/issues" },
|
|
31
|
+
"homepage": "https://github.com/eliedvp/audit-device-tracker#readme",
|
|
32
|
+
"engines": { "node": ">=22" },
|
|
33
|
+
"dependencies": { "bowser": "^2.14.1" },
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/express": "^5.0.6",
|
|
36
|
+
|
|
37
|
+
"@types/node": "^26.6.4",
|
|
38
|
+
"express": "^5.2.1",
|
|
39
|
+
"typescript": "^7.0.2",
|
|
40
|
+
"vitest": "^5.0.3"
|
|
41
|
+
}
|
|
42
|
+
}
|