@sun-asterisk/sunlint 1.3.6 → 1.3.8

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +76 -1
  2. package/config/defaults/default.json +2 -1
  3. package/config/rule-analysis-strategies.js +20 -0
  4. package/config/rules/enhanced-rules-registry.json +230 -43
  5. package/core/analysis-orchestrator.js +9 -5
  6. package/core/file-targeting-service.js +83 -7
  7. package/core/performance-optimizer.js +8 -2
  8. package/package.json +1 -1
  9. package/rules/common/C065_one_behavior_per_test/analyzer.js +851 -0
  10. package/rules/common/C065_one_behavior_per_test/config.json +95 -0
  11. package/rules/common/C073_validate_required_config_on_startup/README.md +110 -0
  12. package/rules/common/C073_validate_required_config_on_startup/analyzer.js +770 -0
  13. package/rules/common/C073_validate_required_config_on_startup/config.json +46 -0
  14. package/rules/common/C073_validate_required_config_on_startup/symbol-based-analyzer.js +370 -0
  15. package/rules/security/S037_cache_headers/README.md +128 -0
  16. package/rules/security/S037_cache_headers/analyzer.js +263 -0
  17. package/rules/security/S037_cache_headers/config.json +50 -0
  18. package/rules/security/S037_cache_headers/regex-based-analyzer.js +463 -0
  19. package/rules/security/S037_cache_headers/symbol-based-analyzer.js +546 -0
  20. package/rules/security/S038_no_version_headers/README.md +234 -0
  21. package/rules/security/S038_no_version_headers/analyzer.js +262 -0
  22. package/rules/security/S038_no_version_headers/config.json +49 -0
  23. package/rules/security/S038_no_version_headers/regex-based-analyzer.js +339 -0
  24. package/rules/security/S038_no_version_headers/symbol-based-analyzer.js +375 -0
  25. package/rules/security/S039_no_session_tokens_in_url/README.md +198 -0
  26. package/rules/security/S039_no_session_tokens_in_url/analyzer.js +262 -0
  27. package/rules/security/S039_no_session_tokens_in_url/config.json +92 -0
  28. package/rules/security/S039_no_session_tokens_in_url/regex-based-analyzer.js +337 -0
  29. package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +436 -0
  30. package/rules/security/S049_short_validity_tokens/analyzer.js +175 -0
  31. package/rules/security/S049_short_validity_tokens/config.json +124 -0
  32. package/rules/security/S049_short_validity_tokens/regex-based-analyzer.js +295 -0
  33. package/rules/security/S049_short_validity_tokens/symbol-based-analyzer.js +389 -0
  34. package/rules/security/S051_password_length_policy/analyzer.js +410 -0
  35. package/rules/security/S051_password_length_policy/config.json +83 -0
  36. package/rules/security/S052_weak_otp_entropy/analyzer.js +403 -0
  37. package/rules/security/S052_weak_otp_entropy/config.json +57 -0
  38. package/rules/security/S054_no_default_accounts/README.md +129 -0
  39. package/rules/security/S054_no_default_accounts/analyzer.js +792 -0
  40. package/rules/security/S054_no_default_accounts/config.json +101 -0
  41. package/rules/security/S056_log_injection_protection/analyzer.js +242 -0
  42. package/rules/security/S056_log_injection_protection/config.json +148 -0
  43. package/rules/security/S056_log_injection_protection/regex-based-analyzer.js +120 -0
  44. package/rules/security/S056_log_injection_protection/symbol-based-analyzer.js +287 -0
  45. package/rules/security/S057_utc_logging/README.md +152 -0
  46. package/rules/security/S057_utc_logging/analyzer.js +457 -0
  47. package/rules/security/S057_utc_logging/config.json +105 -0
  48. package/rules/security/S058_no_ssrf/README.md +180 -0
  49. package/rules/security/S058_no_ssrf/analyzer.js +403 -0
  50. package/rules/security/S058_no_ssrf/config.json +125 -0
@@ -0,0 +1,46 @@
1
+ {
2
+ "id": "C073",
3
+ "name": "C073_validate_required_config_on_startup",
4
+ "category": "configuration",
5
+ "description": "C073 - Validate mandatory configuration at startup and fail fast on invalid/missing values.",
6
+ "severity": "error",
7
+ "enabled": true,
8
+ "semantic": { "enabled": true, "priority": "high", "fallback": "heuristic" },
9
+ "patterns": {
10
+ "include": ["**/*.ts","**/*.tsx","**/*.js","**/*.jsx","**/*.java","**/*.go"],
11
+ "exclude": ["**/*.test.*","**/*.spec.*","**/__tests__/**","**/fixtures/**","**/examples/**"]
12
+ },
13
+ "options": {
14
+ "configModules": {
15
+ "typescript": ["src/config/**","**/config/**","**/bootstrap/**","**/main.ts"],
16
+ "java": ["**/config/**","**/Configuration/**","**/Application.java","**/*Application.java"],
17
+ "go": ["cmd/**","**/config/**","**/main.go"]
18
+ },
19
+ "envAccessors": {
20
+ "typescript": ["process.env.*"],
21
+ "java": ["System.getenv(*)","System.getProperty(*)"],
22
+ "go": ["os.Getenv(*)"]
23
+ },
24
+ "schemaDetectors": {
25
+ "typescript": ["zod","joi","yup","envalid","dotenv-safe","class-validator"],
26
+ "java": ["@ConfigurationProperties","@Validated","jakarta.validation","hibernate.validator"],
27
+ "go": ["github.com/kelseyhightower/envconfig","github.com/spf13/viper"]
28
+ },
29
+ "failFastSignals": {
30
+ "typescript": ["throw new Error(*)","process.exit(1)"],
31
+ "java": ["throw new RuntimeException(*)","SpringApplication.exit(*)","System.exit(1)"],
32
+ "go": ["log.Fatal(*)","panic(*)","os.Exit(1)"]
33
+ },
34
+ "dangerousDefaults": ["|| ''","|| 0","|| 'http://localhost'","?: ''","?: 0"],
35
+ "thresholds": {
36
+ "maxEnvReadsOutsideConfig": 3
37
+ },
38
+ "policy": {
39
+ "requireSchemaOrExplicitChecks": true,
40
+ "requireFailFast": true,
41
+ "forbidEnvReadsOutsideConfig": true,
42
+ "flagDangerousDefaults": true,
43
+ "requireStartupConnectivityChecks": true
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,370 @@
1
+ const { traverse } = require('@babel/traverse');
2
+ const t = require('@babel/types');
3
+
4
+ class C073SymbolBasedAnalyzer {
5
+ constructor(options = {}) {
6
+ this.options = options;
7
+ this.configModules = options.configModules || {};
8
+ this.envAccessors = options.envAccessors || {};
9
+ this.schemaDetectors = options.schemaDetectors || {};
10
+ this.failFastSignals = options.failFastSignals || {};
11
+ this.dangerousDefaults = options.dangerousDefaults || [];
12
+ this.policy = options.policy || {};
13
+ }
14
+
15
+ analyze(ast, filePath, content) {
16
+ const analysis = {
17
+ envAccess: [],
18
+ schemaValidation: [],
19
+ failFastMechanisms: [],
20
+ dangerousDefaults: [],
21
+ connectivityChecks: [],
22
+ imports: [],
23
+ configValidation: false,
24
+ hasFailFast: false
25
+ };
26
+
27
+ const language = this.detectLanguageFromPath(filePath);
28
+
29
+ // Analyze both config files and service files for different patterns
30
+ const isConfigFile = this.isConfigOrStartupFile(filePath, language);
31
+
32
+ // Traverse AST to collect information
33
+ traverse(ast, {
34
+ // Track imports for schema validation libraries
35
+ ImportDeclaration: (path) => {
36
+ const source = path.node.source.value;
37
+ analysis.imports.push(source);
38
+
39
+ // Check for schema validation libraries
40
+ if (this.isSchemaValidationLibrary(source, language)) {
41
+ analysis.schemaValidation.push({
42
+ library: source,
43
+ line: path.node.loc?.start.line || 1
44
+ });
45
+ }
46
+ },
47
+
48
+ // Track environment variable access
49
+ MemberExpression: (path) => {
50
+ if (this.isEnvAccess(path.node, language)) {
51
+ const envProperty = this.getEnvProperty(path.node);
52
+ analysis.envAccess.push({
53
+ variable: envProperty,
54
+ match: `process.env.${envProperty}`,
55
+ line: path.node.loc?.start.line || 1,
56
+ hasDefault: this.hasDefaultValue(path.parent)
57
+ });
58
+ }
59
+ },
60
+
61
+ // Track call expressions for multiple purposes
62
+ CallExpression: (path) => {
63
+ const node = path.node;
64
+
65
+ // Check for require statements
66
+ if (t.isIdentifier(node.callee, { name: 'require' })) {
67
+ const source = node.arguments[0]?.value;
68
+ if (source && this.isSchemaValidationLibrary(source, language)) {
69
+ analysis.schemaValidation.push({
70
+ library: source,
71
+ line: node.loc?.start.line || 1
72
+ });
73
+ }
74
+ }
75
+
76
+ // Check for fail-fast calls (process.exit, throw, etc.)
77
+ if (this.isFailFastCall(node, language)) {
78
+ analysis.failFastMechanisms.push({
79
+ type: this.getFailFastType(node),
80
+ line: node.loc?.start.line || 1
81
+ });
82
+ }
83
+
84
+ // Check for connectivity patterns (ping, connect, health check)
85
+ if (this.isConnectivityCheck(node)) {
86
+ analysis.connectivityChecks.push({
87
+ method: this.getConnectivityMethod(node),
88
+ line: node.loc?.start.line || 1
89
+ });
90
+ }
91
+ },
92
+
93
+ // Track dangerous default patterns
94
+ LogicalExpression: (path) => {
95
+ if (this.isDangerousDefault(path.node)) {
96
+ analysis.dangerousDefaults.push({
97
+ operator: path.node.operator,
98
+ pattern: this.getDangerousDefaultPattern(path.node),
99
+ line: path.node.loc?.start.line || 1
100
+ });
101
+ }
102
+ },
103
+
104
+ // Track conditional expressions with dangerous defaults
105
+ ConditionalExpression: (path) => {
106
+ if (this.isDangerousConditionalDefault(path.node)) {
107
+ analysis.dangerousDefaults.push({
108
+ type: 'conditional',
109
+ pattern: this.getDangerousDefaultPattern(path.node),
110
+ line: path.node.loc?.start.line || 1
111
+ });
112
+ }
113
+ },
114
+
115
+ // Check for validation patterns
116
+ IfStatement: (path) => {
117
+ if (this.isConfigValidation(path.node, analysis.envAccess)) {
118
+ analysis.configValidation = true;
119
+ }
120
+ },
121
+
122
+ // Check for throw statements with configuration errors
123
+ ThrowStatement: (path) => {
124
+ if (this.isConfigRelatedError(path.node)) {
125
+ analysis.failFastMechanisms.push({
126
+ type: 'throw',
127
+ line: path.node.loc?.start.line || 1
128
+ });
129
+ }
130
+ }
131
+ });
132
+
133
+ return analysis;
134
+ }
135
+
136
+ detectLanguageFromPath(filePath) {
137
+ const ext = filePath.split('.').pop()?.toLowerCase();
138
+ if (['ts', 'tsx', 'js', 'jsx'].includes(ext)) return 'typescript';
139
+ if (['java'].includes(ext)) return 'java';
140
+ if (['go'].includes(ext)) return 'go';
141
+ return null;
142
+ }
143
+
144
+ isConfigOrStartupFile(filePath, language) {
145
+ const configPatterns = this.configModules[language] || [];
146
+ return configPatterns.some(pattern => {
147
+ const globPattern = pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*');
148
+ const regex = new RegExp(globPattern.replace(/\//g, '\\/'));
149
+ return regex.test(filePath);
150
+ });
151
+ }
152
+
153
+ isSchemaValidationLibrary(source, language) {
154
+ const libraries = this.schemaDetectors[language] || [];
155
+ return libraries.some(lib => source.includes(lib));
156
+ }
157
+
158
+ isEnvAccess(node, language) {
159
+ if (language === 'typescript') {
160
+ // process.env.VARIABLE
161
+ return (
162
+ t.isMemberExpression(node) &&
163
+ t.isMemberExpression(node.object) &&
164
+ t.isIdentifier(node.object.object, { name: 'process' }) &&
165
+ t.isIdentifier(node.object.property, { name: 'env' })
166
+ );
167
+ }
168
+ return false;
169
+ }
170
+
171
+ getEnvProperty(node) {
172
+ if (t.isIdentifier(node.property)) {
173
+ return node.property.name;
174
+ }
175
+ if (t.isStringLiteral(node.property)) {
176
+ return node.property.value;
177
+ }
178
+ return 'unknown';
179
+ }
180
+
181
+ hasDefaultValue(parent) {
182
+ return (
183
+ t.isLogicalExpression(parent) ||
184
+ t.isConditionalExpression(parent) ||
185
+ (t.isAssignmentExpression(parent) && parent.right)
186
+ );
187
+ }
188
+
189
+ isFailFastCall(node, language) {
190
+ if (language === 'typescript') {
191
+ // process.exit(1)
192
+ if (
193
+ t.isMemberExpression(node.callee) &&
194
+ t.isIdentifier(node.callee.object, { name: 'process' }) &&
195
+ t.isIdentifier(node.callee.property, { name: 'exit' })
196
+ ) {
197
+ return true;
198
+ }
199
+ }
200
+ return false;
201
+ }
202
+
203
+ getFailFastType(node) {
204
+ if (t.isMemberExpression(node.callee)) {
205
+ if (t.isIdentifier(node.callee.property, { name: 'exit' })) {
206
+ return 'process.exit';
207
+ }
208
+ }
209
+ if (t.isThrowStatement(node)) {
210
+ return 'throw';
211
+ }
212
+ return 'unknown';
213
+ }
214
+
215
+ isConnectivityCheck(node) {
216
+ if (!t.isCallExpression(node)) return false;
217
+
218
+ // Check for common connectivity patterns
219
+ const patterns = [
220
+ 'ping', 'connect', 'healthCheck', 'testConnection',
221
+ 'validateConnection', 'checkHealth', 'authenticate'
222
+ ];
223
+
224
+ if (t.isMemberExpression(node.callee)) {
225
+ const methodName = node.callee.property?.name;
226
+ return patterns.includes(methodName);
227
+ }
228
+
229
+ if (t.isIdentifier(node.callee)) {
230
+ return patterns.includes(node.callee.name);
231
+ }
232
+
233
+ return false;
234
+ }
235
+
236
+ getConnectivityMethod(node) {
237
+ if (t.isMemberExpression(node.callee)) {
238
+ return node.callee.property?.name || 'unknown';
239
+ }
240
+ if (t.isIdentifier(node.callee)) {
241
+ return node.callee.name;
242
+ }
243
+ return 'unknown';
244
+ }
245
+
246
+ isDangerousDefault(node) {
247
+ if (!t.isLogicalExpression(node, { operator: '||' })) {
248
+ return false;
249
+ }
250
+
251
+ const rightValue = this.getDefaultValue(node.right);
252
+ const dangerousPatterns = [
253
+ "''", '""', '0', 'null', 'undefined',
254
+ "'localhost'", "'http://localhost'", "'dev'", "'development'"
255
+ ];
256
+
257
+ return dangerousPatterns.includes(rightValue);
258
+ }
259
+
260
+ isDangerousConditionalDefault(node) {
261
+ if (!t.isConditionalExpression(node)) return false;
262
+
263
+ const consequent = this.getDefaultValue(node.consequent);
264
+ const alternate = this.getDefaultValue(node.alternate);
265
+
266
+ const dangerousPatterns = [
267
+ "''", '""', '0', 'null', 'undefined',
268
+ "'localhost'", "'http://localhost'", "'dev'", "'development'"
269
+ ];
270
+
271
+ return dangerousPatterns.includes(consequent) || dangerousPatterns.includes(alternate);
272
+ }
273
+
274
+ getDangerousDefaultPattern(node) {
275
+ if (t.isLogicalExpression(node)) {
276
+ return this.getDefaultValue(node.right);
277
+ }
278
+ if (t.isConditionalExpression(node)) {
279
+ const consequent = this.getDefaultValue(node.consequent);
280
+ const alternate = this.getDefaultValue(node.alternate);
281
+ return `${consequent} ? ${alternate}`;
282
+ }
283
+ return 'unknown';
284
+ }
285
+
286
+ getDefaultValue(node) {
287
+ if (t.isStringLiteral(node)) {
288
+ return `'${node.value}'`;
289
+ }
290
+ if (t.isNumericLiteral(node)) {
291
+ return node.value.toString();
292
+ }
293
+ if (t.isNullLiteral(node)) {
294
+ return 'null';
295
+ }
296
+ if (t.isIdentifier(node, { name: 'undefined' })) {
297
+ return 'undefined';
298
+ }
299
+ return 'unknown';
300
+ }
301
+
302
+ isConfigValidation(node, envAccess) {
303
+ // Simple heuristic: if an if statement contains a check for environment variables
304
+ // and has a throw or process.exit in the consequent, it's likely validation
305
+ if (t.isIfStatement(node)) {
306
+ const test = node.test;
307
+ const consequent = node.consequent;
308
+
309
+ // Check if test involves environment variables
310
+ const involvesEnv = this.containsEnvAccess(test);
311
+
312
+ // Check if consequent has fail-fast behavior
313
+ const hasFailFast = this.containsFailFast(consequent);
314
+
315
+ return involvesEnv && hasFailFast;
316
+ }
317
+
318
+ return false;
319
+ }
320
+
321
+ containsEnvAccess(node) {
322
+ // Simple traversal to check if node contains process.env access
323
+ let hasEnvAccess = false;
324
+
325
+ traverse(node, {
326
+ MemberExpression: (path) => {
327
+ if (this.isEnvAccess(path.node, 'typescript')) {
328
+ hasEnvAccess = true;
329
+ path.stop();
330
+ }
331
+ }
332
+ }, null, {});
333
+
334
+ return hasEnvAccess;
335
+ }
336
+
337
+ containsFailFast(node) {
338
+ // Check if node contains fail-fast patterns
339
+ let hasFailFast = false;
340
+
341
+ traverse(node, {
342
+ CallExpression: (path) => {
343
+ if (this.isFailFastCall(path.node, 'typescript')) {
344
+ hasFailFast = true;
345
+ path.stop();
346
+ }
347
+ },
348
+ ThrowStatement: () => {
349
+ hasFailFast = true;
350
+ }
351
+ }, null, {});
352
+
353
+ return hasFailFast;
354
+ }
355
+
356
+ isConfigRelatedError(node) {
357
+ // Check if the error message is configuration-related
358
+ if (t.isThrowStatement(node) && t.isNewExpression(node.argument)) {
359
+ const args = node.argument.arguments;
360
+ if (args.length > 0 && t.isStringLiteral(args[0])) {
361
+ const message = args[0].value.toLowerCase();
362
+ const configKeywords = ['config', 'environment', 'env', 'missing', 'required', 'invalid'];
363
+ return configKeywords.some(keyword => message.includes(keyword));
364
+ }
365
+ }
366
+ return false;
367
+ }
368
+ }
369
+
370
+ module.exports = C073SymbolBasedAnalyzer;
@@ -0,0 +1,128 @@
1
+ # S037 - Configure Comprehensive Cache Headers to Prevent Sensitive Data Leakage
2
+
3
+ ## Overview
4
+
5
+ Rule S037 ensures that sensitive HTTP responses explicitly disable caching using a complete set of anti-caching headers:
6
+
7
+ - `Cache-Control: no-store, no-cache, must-revalidate`
8
+ - `Pragma: no-cache`
9
+ - `Expires: 0`
10
+
11
+ These prevent browsers and intermediate proxies from persisting sensitive content (e.g., authenticated pages, profile data, tokens) in cache storage where it could later be exposed.
12
+
13
+ ## Framework Support
14
+
15
+ This rule supports multiple web frameworks:
16
+
17
+ - **Express.js**: Route handlers using `app.get()`, `router.post()`, etc.
18
+ - **Next.js**: API routes (`export default function handler`) and App Router (`export async function GET`)
19
+ - **NestJS**: Controller methods with decorators (`@Get()`, `@Post()`, etc.)
20
+ - **Nuxt.js**: Server routes (`export default defineEventHandler`)
21
+
22
+ ## Security Impact
23
+
24
+ Without proper cache headers, authenticated or confidential data may remain in browser cache or be served to other users (in shared environments, kiosk systems, or via back/forward navigation). Attackers may retrieve cached responses, exposing session data or personal information.
25
+
26
+ ## Rule Details
27
+
28
+ ### Required Header Set
29
+
30
+ All of the following must be present for sensitive responses:
31
+
32
+ | Header | Required Value / Directives |
33
+ | ------------- | --------------------------------------------------------------------------- |
34
+ | Cache-Control | `no-store, no-cache, must-revalidate` (order flexible, directives required) |
35
+ | Pragma | `no-cache` |
36
+ | Expires | `0` (or a past date) |
37
+
38
+ ### Sensitive Response Indicators (heuristic)
39
+
40
+ The rule assumes a response is sensitive when code references identifiers like: `session`, `auth`, `token`, `jwt`, `csrf`, `user`, `profile`, `payment`, `account`.
41
+
42
+ ### Detected Patterns
43
+
44
+ ✅ **Compliant Examples:**
45
+
46
+ ```typescript
47
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
48
+ res.setHeader("Pragma", "no-cache");
49
+ res.setHeader("Expires", "0");
50
+ res.json({ user: profile });
51
+ ```
52
+
53
+ ```typescript
54
+ response.set({
55
+ "Cache-Control": "no-store, no-cache, must-revalidate",
56
+ Pragma: "no-cache",
57
+ Expires: "0",
58
+ });
59
+ ```
60
+
61
+ ❌ **Violation Examples:**
62
+
63
+ ```typescript
64
+ // Missing Cache-Control directives
65
+ res.setHeader("Cache-Control", "no-cache");
66
+ res.setHeader("Pragma", "no-cache");
67
+ res.setHeader("Expires", "0");
68
+ ```
69
+
70
+ ```typescript
71
+ // Missing Pragma and Expires
72
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
73
+ ```
74
+
75
+ ```typescript
76
+ // Missing all anti-cache headers
77
+ res.json({ auth: token, profile });
78
+ ```
79
+
80
+ ### Partial Cache-Control Values
81
+
82
+ The rule flags missing directives individually when `Cache-Control` is present but incomplete.
83
+
84
+ | Example Value | Result |
85
+ | ------------------------------------- | ----------------------------------------- |
86
+ | `no-cache` | ❌ Missing: `no-store`, `must-revalidate` |
87
+ | `no-store, no-cache` | ❌ Missing: `must-revalidate` |
88
+ | `no-store, no-cache, must-revalidate` | ✅ Valid |
89
+
90
+ ## Analysis Approach
91
+
92
+ ### Symbol-Based (Primary)
93
+
94
+ Parses AST to collect header setting calls across a file, then evaluates completeness.
95
+
96
+ ### Regex-Based (Fallback)
97
+
98
+ Scans for literal `setHeader`/`set` calls to approximate detection when semantic engine unavailable.
99
+
100
+ ## Configuration
101
+
102
+ See `config.json` for adjustable keys:
103
+
104
+ - `validation.required` – mandatory headers & directives
105
+ - `validation.sensitiveIndicators` – triggers stricter enforcement when detected
106
+ - `patterns.include/exclude` – file targeting
107
+
108
+ ## Best Practices
109
+
110
+ 1. Always send complete anti-cache headers for authenticated pages.
111
+ 2. Never rely only on `Pragma` or only `Cache-Control`.
112
+ 3. Include `no-store` for maximum protection (prevents any storage).
113
+ 4. Pair with other security headers (CSP, X-Content-Type-Options, etc.).
114
+ 5. Re-check reverse proxy / CDN behavior (may override headers).
115
+
116
+ ## Related Rules
117
+
118
+ - **S031**: Secure flag for cookies
119
+ - **S032**: HttpOnly flag for cookies
120
+ - **S033**: SameSite attribute
121
+ - **S034**: \_\_Host- prefix
122
+ - **S035**: Path attribute
123
+
124
+ ## References
125
+
126
+ - MDN: HTTP Caching
127
+ - OWASP: Sensitive Data Exposure / Cryptographic Failures
128
+ - RFC 7234 - HTTP/1.1 Caching