graphql-sentinel 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Stuart
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,530 @@
1
+ # graphql-sentinel
2
+
3
+ [![CI](https://github.com/mstuart/graphql-sentinel/actions/workflows/ci.yml/badge.svg)](https://github.com/mstuart/graphql-sentinel/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/graphql-sentinel.svg)](https://www.npmjs.com/package/graphql-sentinel)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ Comprehensive GraphQL security scanner and runtime shield. Detect vulnerabilities in your GraphQL API and protect it at runtime with validation rules and rate limiting.
8
+
9
+ ## Quick Start
10
+
11
+ Scan any GraphQL endpoint for security vulnerabilities:
12
+
13
+ ```bash
14
+ npx graphql-sentinel scan https://api.example.com/graphql
15
+ ```
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install graphql-sentinel graphql
21
+ ```
22
+
23
+ ## Requirements
24
+
25
+ - Node.js >= 18.0.0
26
+ - `graphql` >= 16.0.0 (peer dependency)
27
+ - `graphql-yoga` >= 5.0.0 (optional, for Yoga plugin)
28
+ - `@apollo/server` >= 4.0.0 (optional, for Apollo plugin)
29
+ - TypeScript >= 5.0 (optional, for type definitions)
30
+
31
+ Fully written in TypeScript with complete type exports for all public APIs.
32
+
33
+ ## CLI Usage
34
+
35
+ ### Scan an endpoint
36
+
37
+ ```bash
38
+ # Basic scan with terminal output
39
+ graphql-sentinel scan https://api.example.com/graphql
40
+
41
+ # JSON output
42
+ graphql-sentinel scan https://api.example.com/graphql --format json
43
+
44
+ # HTML report saved to file
45
+ graphql-sentinel scan https://api.example.com/graphql --format html --output report.html
46
+
47
+ # SARIF report for GitHub Security tab
48
+ graphql-sentinel scan https://api.example.com/graphql --format sarif --output report.sarif.json
49
+
50
+ # Security dashboard
51
+ graphql-sentinel scan https://api.example.com/graphql --format dashboard --output dashboard.html
52
+
53
+ # With custom headers
54
+ graphql-sentinel scan https://api.example.com/graphql -H "Authorization: Bearer token123"
55
+
56
+ # Run specific checks only
57
+ graphql-sentinel scan https://api.example.com/graphql --checks introspection,csrf,depth-limit,auth-bypass
58
+
59
+ # Custom timeout per check (in ms)
60
+ graphql-sentinel scan https://api.example.com/graphql --timeout 15000
61
+ ```
62
+
63
+ The CLI exits with code `1` if any critical or high severity issues are found, making it suitable for CI/CD pipelines.
64
+
65
+ ### Start a security proxy
66
+
67
+ ```bash
68
+ # Basic proxy with depth limiting
69
+ graphql-sentinel proxy https://upstream-api.example.com/graphql --max-depth 10
70
+
71
+ # Full shield configuration
72
+ graphql-sentinel proxy https://upstream-api.example.com/graphql \
73
+ --port 4000 \
74
+ --max-depth 10 \
75
+ --max-complexity 1000 \
76
+ --max-aliases 15 \
77
+ --disable-introspection \
78
+ --rate-limit-window 60000 \
79
+ --rate-limit-max 100
80
+
81
+ # Forward auth headers to upstream
82
+ graphql-sentinel proxy https://upstream-api.example.com/graphql \
83
+ -H "X-API-Key: secret"
84
+ ```
85
+
86
+ ## Security Checks
87
+
88
+ | Check | Severity | Description |
89
+ |-------|----------|-------------|
90
+ | `introspection` | Medium | Detects if introspection is enabled, exposing the full schema |
91
+ | `depth-limit` | High | Tests for absence of query depth limits (DoS vector) |
92
+ | `batch-attack` | Medium | Checks if batch queries are accepted (amplification attacks) |
93
+ | `field-suggestion` | Low | Detects field suggestions in error messages (schema enumeration) |
94
+ | `alias-overloading` | Medium | Tests if unlimited aliases are accepted (DoS vector) |
95
+ | `csrf` | High | Checks if queries are accepted via GET requests (CSRF risk) |
96
+ | `auth-bypass` | High | Tests for authorization bypass by sending unauthenticated requests |
97
+
98
+ ### Authorization Bypass Detection
99
+
100
+ The `auth-bypass` check tests your endpoint for missing or improperly configured authorization:
101
+
102
+ 1. Sends a request without any auth headers
103
+ 2. Sends a request with an empty Authorization header
104
+ 3. Sends a request with an invalid Bearer token
105
+ 4. If auth headers are provided, compares authenticated vs unauthenticated responses
106
+
107
+ If any unauthenticated request returns data, it flags a potential bypass. Public APIs (no auth configured) are reported as `info` severity rather than failures.
108
+
109
+ ## Shield Middleware
110
+
111
+ Protect your GraphQL server at runtime with validation rules.
112
+
113
+ ### GraphQL Yoga
114
+
115
+ ```typescript
116
+ import { createYoga, createSchema } from 'graphql-yoga';
117
+ import { useSentinelShield } from 'graphql-sentinel';
118
+
119
+ const yoga = createYoga({
120
+ schema: createSchema({ /* ... */ }),
121
+ plugins: [
122
+ useSentinelShield({
123
+ maxDepth: 10,
124
+ maxComplexity: 1000,
125
+ maxAliases: 15,
126
+ disableIntrospection: true,
127
+ rateLimit: { window: 60000, max: 100 },
128
+ }),
129
+ ],
130
+ });
131
+ ```
132
+
133
+ ### Apollo Server
134
+
135
+ ```typescript
136
+ import { ApolloServer } from '@apollo/server';
137
+ import { sentinelApolloPlugin } from 'graphql-sentinel';
138
+
139
+ const server = new ApolloServer({
140
+ typeDefs,
141
+ resolvers,
142
+ plugins: [
143
+ sentinelApolloPlugin({
144
+ maxDepth: 10,
145
+ maxComplexity: 1000,
146
+ disableIntrospection: true,
147
+ }),
148
+ ],
149
+ });
150
+ ```
151
+
152
+ ### Express Middleware
153
+
154
+ ```typescript
155
+ import express from 'express';
156
+ import { GraphQLSchema } from 'graphql';
157
+ import { sentinelMiddleware } from 'graphql-sentinel';
158
+
159
+ const app = express();
160
+ app.use(express.json());
161
+
162
+ // Apply before your GraphQL middleware
163
+ app.use('/graphql', sentinelMiddleware(schema, {
164
+ maxDepth: 10,
165
+ maxAliases: 15,
166
+ disableIntrospection: true,
167
+ }));
168
+ ```
169
+
170
+ ### Field-Level Authorization
171
+
172
+ Enforce fine-grained authorization at the field level using GraphQL validation rules:
173
+
174
+ ```typescript
175
+ import { createShield, createFieldAuthRule } from 'graphql-sentinel';
176
+
177
+ const shield = createShield({
178
+ maxDepth: 10,
179
+ fieldAuth: {
180
+ rules: {
181
+ 'Query.users': { requireAuth: true, roles: ['admin'] },
182
+ 'Query.user': { requireAuth: true, permissions: ['read:users'] },
183
+ 'Mutation.deleteUser': { requireAuth: true, roles: ['admin'] },
184
+ 'User.email': { requireAuth: true },
185
+ },
186
+ extractContext: (context) => {
187
+ // Extract user info from your GraphQL context
188
+ const user = (context as any)?.user;
189
+ if (!user) return null;
190
+ return {
191
+ authenticated: true,
192
+ roles: user.roles || [],
193
+ permissions: user.permissions || [],
194
+ };
195
+ },
196
+ },
197
+ });
198
+
199
+ // Use with graphql's validate()
200
+ const errors = validate(schema, parse(query), shield.validationRules);
201
+ ```
202
+
203
+ The `createFieldAuthRule` can also be used standalone:
204
+
205
+ ```typescript
206
+ import { createFieldAuthRule } from 'graphql-sentinel';
207
+
208
+ const rule = createFieldAuthRule({
209
+ rules: {
210
+ 'Query.sensitiveData': { requireAuth: true, roles: ['admin'] },
211
+ },
212
+ extractContext: (ctx) => /* ... */,
213
+ });
214
+
215
+ // Add to your validation rules array
216
+ const errors = validate(schema, document, [rule]);
217
+ ```
218
+
219
+ ## Proxy Mode
220
+
221
+ Run graphql-sentinel as a standalone reverse proxy that enforces security rules before forwarding requests to your upstream GraphQL server:
222
+
223
+ ```typescript
224
+ import { createProxyServer, startProxy } from 'graphql-sentinel';
225
+
226
+ // Quick start
227
+ await startProxy({
228
+ target: 'https://upstream-api.example.com/graphql',
229
+ port: 4000,
230
+ shield: {
231
+ maxDepth: 10,
232
+ maxComplexity: 1000,
233
+ maxAliases: 15,
234
+ disableIntrospection: true,
235
+ rateLimit: { window: 60000, max: 100 },
236
+ },
237
+ headers: { 'X-API-Key': 'upstream-key' },
238
+ });
239
+
240
+ // Or get the raw http.Server for custom configuration
241
+ const server = createProxyServer({
242
+ target: 'https://upstream-api.example.com/graphql',
243
+ port: 4000,
244
+ shield: { maxDepth: 10 },
245
+ });
246
+ server.listen(4000);
247
+ ```
248
+
249
+ The proxy:
250
+ - Parses and validates all incoming GraphQL queries against shield rules
251
+ - Blocks queries that exceed depth, complexity, or alias limits
252
+ - Blocks introspection queries when configured
253
+ - Enforces rate limiting per client IP
254
+ - Forwards valid queries to the upstream server
255
+ - Handles CORS headers automatically
256
+ - Returns `400` for blocked queries with detailed error messages
257
+ - Returns `429` for rate-limited requests
258
+
259
+ ## Report Formats
260
+
261
+ ### Terminal
262
+
263
+ ANSI-colored output for terminal/CLI usage.
264
+
265
+ ### JSON
266
+
267
+ Machine-readable JSON output of the full scan report.
268
+
269
+ ### HTML
270
+
271
+ Self-contained HTML report with styled results.
272
+
273
+ ### SARIF (Static Analysis Results Interchange Format)
274
+
275
+ SARIF 2.1.0 compliant output for integration with GitHub's Security tab:
276
+
277
+ ```bash
278
+ graphql-sentinel scan https://api.example.com/graphql --format sarif --output results.sarif
279
+ ```
280
+
281
+ Upload to GitHub Security tab:
282
+
283
+ ```yaml
284
+ - uses: github/codeql-action/upload-sarif@v3
285
+ with:
286
+ sarif_file: results.sarif
287
+ ```
288
+
289
+ ### Dashboard
290
+
291
+ A rich, interactive security dashboard with:
292
+
293
+ - **Security posture score** (0-100) weighted by severity
294
+ - **Executive summary** suitable for management reporting
295
+ - **Category breakdown** (Authorization, DoS, Information Disclosure)
296
+ - **Expandable check details** with remediation guidance
297
+ - **Vulnerability timeline** tracking when multiple reports are provided
298
+ - **localStorage persistence** for building history across browser sessions
299
+ - Dark theme with professional styling, fully self-contained (no external dependencies)
300
+
301
+ ```bash
302
+ graphql-sentinel scan https://api.example.com/graphql --format dashboard --output dashboard.html
303
+ ```
304
+
305
+ Programmatic usage with multiple reports for timeline tracking:
306
+
307
+ ```typescript
308
+ import { generateDashboard, runScan } from 'graphql-sentinel';
309
+
310
+ const reports = [previousReport, currentReport];
311
+ const html = generateDashboard(reports, { title: 'My API Security Dashboard' });
312
+ ```
313
+
314
+ ## GitHub Action
315
+
316
+ Use graphql-sentinel as a reusable GitHub Action in your CI/CD pipelines:
317
+
318
+ ```yaml
319
+ jobs:
320
+ security-scan:
321
+ runs-on: ubuntu-latest
322
+ steps:
323
+ - uses: mstuart/graphql-sentinel/.github/actions/scan@main
324
+ with:
325
+ endpoint: 'https://api.example.com/graphql'
326
+ format: 'sarif'
327
+ fail-on-severity: 'high'
328
+ headers: |
329
+ Authorization: Bearer ${{ secrets.API_TOKEN }}
330
+ ```
331
+
332
+ ### Action Inputs
333
+
334
+ | Input | Required | Default | Description |
335
+ |-------|----------|---------|-------------|
336
+ | `endpoint` | Yes | - | GraphQL endpoint URL to scan |
337
+ | `format` | No | `terminal` | Output format (terminal, json, html, sarif) |
338
+ | `checks` | No | all | Comma-separated list of checks to run |
339
+ | `fail-on-severity` | No | `high` | Minimum severity to fail the build |
340
+ | `headers` | No | - | Headers, one per line ("Key: Value") |
341
+ | `timeout` | No | `10000` | Timeout per check in milliseconds |
342
+
343
+ ### Action Outputs
344
+
345
+ | Output | Description |
346
+ |--------|-------------|
347
+ | `report` | Path to the generated report file |
348
+ | `passed` | Whether the scan passed (`true`/`false`) |
349
+
350
+ The action automatically uploads the report as a build artifact named `sentinel-security-report`.
351
+
352
+ ## Programmatic API
353
+
354
+ ### Scanner
355
+
356
+ ```typescript
357
+ import { runScan } from 'graphql-sentinel';
358
+
359
+ const report = await runScan({
360
+ endpoint: 'https://api.example.com/graphql',
361
+ headers: { Authorization: 'Bearer token' },
362
+ checks: ['introspection', 'depth-limit', 'csrf', 'auth-bypass'],
363
+ timeout: 10000,
364
+ });
365
+
366
+ console.log(`Found ${report.summary.failed} issues`);
367
+ ```
368
+
369
+ ### Shield (Standalone)
370
+
371
+ ```typescript
372
+ import { createShield } from 'graphql-sentinel';
373
+ import { validate, parse } from 'graphql';
374
+
375
+ const shield = createShield({
376
+ maxDepth: 10,
377
+ maxComplexity: 1000,
378
+ maxAliases: 15,
379
+ disableIntrospection: true,
380
+ rateLimit: { window: 60000, max: 100 },
381
+ });
382
+
383
+ // Use validation rules with graphql's validate()
384
+ const errors = validate(schema, parse(query), shield.validationRules);
385
+
386
+ // Use rate limiter
387
+ if (shield.rateLimiter) {
388
+ const { allowed, remaining } = shield.rateLimiter.check(clientIp, queryCost);
389
+ if (!allowed) {
390
+ throw new Error('Rate limit exceeded');
391
+ }
392
+ }
393
+ ```
394
+
395
+ ### Report Generation
396
+
397
+ ```typescript
398
+ import { runScan, generateReport, generateDashboard, generateSarifReport } from 'graphql-sentinel';
399
+
400
+ const report = await runScan({ endpoint: 'https://api.example.com/graphql' });
401
+
402
+ // Terminal output with ANSI colors
403
+ console.log(generateReport(report, 'terminal'));
404
+
405
+ // JSON
406
+ const json = generateReport(report, 'json');
407
+
408
+ // Self-contained HTML
409
+ const html = generateReport(report, 'html');
410
+
411
+ // SARIF for GitHub Security tab
412
+ const sarif = generateReport(report, 'sarif');
413
+
414
+ // Dashboard with timeline tracking
415
+ const dashboard = generateDashboard([report], { title: 'Security Dashboard' });
416
+ ```
417
+
418
+ ## Configuration Reference
419
+
420
+ ### ScannerConfig
421
+
422
+ | Option | Type | Default | Description |
423
+ |--------|------|---------|-------------|
424
+ | `endpoint` | `string` | required | GraphQL endpoint URL |
425
+ | `headers` | `Record<string, string>` | `undefined` | Custom HTTP headers |
426
+ | `checks` | `string[]` | all checks | List of check names to run |
427
+ | `timeout` | `number` | `10000` | Timeout per check in milliseconds |
428
+
429
+ ### ShieldConfig
430
+
431
+ | Option | Type | Default | Description |
432
+ |--------|------|---------|-------------|
433
+ | `maxDepth` | `number` | `undefined` | Maximum query nesting depth |
434
+ | `maxComplexity` | `number` | `undefined` | Maximum query complexity score |
435
+ | `maxAliases` | `number` | `undefined` | Maximum number of aliases per query |
436
+ | `disableIntrospection` | `boolean` | `false` | Block introspection queries |
437
+ | `costLimit` | `number` | `undefined` | Maximum query cost |
438
+ | `rateLimit.window` | `number` | `undefined` | Rate limit window in milliseconds |
439
+ | `rateLimit.max` | `number` | `undefined` | Maximum cost per window |
440
+ | `fieldAuth` | `FieldAuthConfig` | `undefined` | Field-level authorization rules |
441
+
442
+ ### ProxyConfig
443
+
444
+ | Option | Type | Default | Description |
445
+ |--------|------|---------|-------------|
446
+ | `target` | `string` | required | Upstream GraphQL endpoint URL |
447
+ | `port` | `number` | `4000` | Proxy listening port |
448
+ | `shield` | `ShieldConfig` | required | Shield configuration |
449
+ | `headers` | `Record<string, string>` | `undefined` | Headers to forward to upstream |
450
+ | `cors` | `boolean` | `true` | Enable CORS headers |
451
+
452
+ ### FieldAuthConfig
453
+
454
+ | Option | Type | Description |
455
+ |--------|------|-------------|
456
+ | `rules` | `Record<string, FieldAuthRule>` | Map of `TypeName.fieldName` to auth rules |
457
+ | `extractContext` | `(context) => UserContext \| null` | Function to extract user context |
458
+
459
+ ### FieldAuthRule
460
+
461
+ | Option | Type | Description |
462
+ |--------|------|-------------|
463
+ | `requireAuth` | `boolean` | Whether authentication is required |
464
+ | `roles` | `string[]` | Required roles (any match grants access) |
465
+ | `permissions` | `string[]` | Required permissions (any match grants access) |
466
+
467
+ ## Comparison with graphql-armor
468
+
469
+ [graphql-armor](https://github.com/Escape-Technologies/graphql-armor) is an excellent runtime-only shield. graphql-sentinel provides a broader security toolkit:
470
+
471
+ | Feature | graphql-sentinel | graphql-armor |
472
+ |---------|-----------------|---------------|
473
+ | Runtime shield (depth, complexity, aliases) | Yes | Yes |
474
+ | Security scanner (7 automated checks) | Yes | No |
475
+ | CLI for CI/CD pipelines | Yes | No |
476
+ | SARIF reports for GitHub Security tab | Yes | No |
477
+ | Interactive security dashboard | Yes | No |
478
+ | Reverse proxy mode | Yes | No |
479
+ | Reusable GitHub Action | Yes | No |
480
+ | Field-level authorization | Yes | No |
481
+ | Express middleware | Yes | No |
482
+
483
+ Choose graphql-armor if you only need runtime protection. Choose graphql-sentinel if you also want scanning, reporting, CI integration, or proxy deployment.
484
+
485
+ ## API Reference
486
+
487
+ ### Scanner
488
+
489
+ - `runScan(config: ScannerConfig): Promise<ScanReport>` - Run security checks against an endpoint
490
+
491
+ ### Shield
492
+
493
+ - `createShield(config: ShieldConfig): Shield` - Create shield with validation rules and rate limiter
494
+ - `createDepthLimitRule(maxDepth?: number)` - Create depth limit validation rule
495
+ - `createComplexityRule(config?: ComplexityConfig)` - Create complexity validation rule
496
+ - `createAliasLimitRule(maxAliases?: number)` - Create alias limit validation rule
497
+ - `createIntrospectionControlRule()` - Create introspection blocking rule
498
+ - `createRateLimiter(config: RateLimitConfig)` - Create sliding window rate limiter
499
+ - `createFieldAuthRule(config: FieldAuthConfig)` - Create field-level authorization rule
500
+
501
+ ### Proxy
502
+
503
+ - `createProxyServer(config: ProxyConfig): http.Server` - Create proxy server instance
504
+ - `startProxy(config: ProxyConfig): Promise<http.Server>` - Create and start proxy server
505
+
506
+ ### Plugins
507
+
508
+ - `useSentinelShield(config?: ShieldConfig)` - GraphQL Yoga plugin
509
+ - `sentinelApolloPlugin(config?: ShieldConfig)` - Apollo Server plugin
510
+ - `sentinelMiddleware(schema, config?: ShieldConfig)` - Express middleware
511
+
512
+ ### Reporter
513
+
514
+ - `generateReport(report: ScanReport, format: 'json' | 'terminal' | 'html' | 'sarif' | 'dashboard'): string` - Generate formatted report
515
+ - `generateSarifReport(report: ScanReport): string` - Generate SARIF 2.1.0 report
516
+ - `generateDashboard(reports: ScanReport[], config?): string` - Generate security dashboard
517
+ - `calculatePostureScore(results: ScanResult[]): number` - Calculate security posture score (0-100)
518
+
519
+ ## Contributing
520
+
521
+ 1. Fork the repository
522
+ 2. Create your feature branch (`git checkout -b feature/my-feature`)
523
+ 3. Run tests (`npm test`)
524
+ 4. Commit your changes (`git commit -am 'feat: add my feature'`)
525
+ 5. Push to the branch (`git push origin feature/my-feature`)
526
+ 6. Open a Pull Request
527
+
528
+ ## License
529
+
530
+ [MIT](LICENSE)