Repository: stoplightio/spectral (ts)

Issue:
Make sure op-security-defined rules work correctly with AND auth
**Chore summary**
- [ ] We have rules for oas3/oas2 op-security-defined. Make sure these work correctly for AND Auth usecases/syntax. If it doesn't update them to make sure it works. 
- [ ] Within the oas3/oas2 op-security-defined rules: Validate if scopes specified within a security requirements object are defined within security schemes


Pull request description:
feat(rulesets): add scope validation to oas{2,3}-operation-security-defined rules

Closes #2529 

TBH we should probably rename that rule since it also validates top-level securities.

**Checklist**

- [x] Tests added / updated
- [ ] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No


Patch:
diff --git a/packages/rulesets/src/oas/functions/oasSecurityDefined.ts b/packages/rulesets/src/oas/functions/oasSecurityDefined.ts
index bb273fd7..1043a9bc 100644
--- a/packages/rulesets/src/oas/functions/oasSecurityDefined.ts
+++ b/packages/rulesets/src/oas/functions/oasSecurityDefined.ts
@@ -6,10 +6,16 @@ type Options = {
   oasVersion: 2 | 3;
 };
 
-export default createRulesetFunction<Record<string, unknown>, Options>(
+export default createRulesetFunction<Record<string, string[]>, Options>(
   {
     input: {
       type: 'object',
+      additionalProperties: {
+        type: 'array',
+        items: {
+          type: 'string',
+        },
+      },
     },
     options: {
       type: 'object',
@@ -45,9 +51,41 @@ export default createRulesetFunction<Record<string, unknown>, Options>(
           message: `${object} "security" values must match a scheme defined in the "${location}" object.`,
           path: [...path, schemeName],
         });
+
+        continue;
+      }
+
+      const scope = input[schemeName];
+      for (let i = 0; i < scope.length; i++) {
+        const scopeName = scope[i];
+        if (!isScopeDefined(oasVersion, scopeName, allDefs[schemeName])) {
+          results ??= [];
+          results.push({
+            message: `"${scopeName}" must be listed among scopes.`,
+            path: [...path, schemeName, i],
+          });
+        }
       }
     }
 
     return results;
   },
 );
+
+function isScopeDefined(oasVersion: 2 | 3, scopeName: string, securityScheme: unknown): boolean {
+  if (!isPlainObject(securityScheme)) return false;
+
+  if (oasVersion === 2) {
+    return isPlainObject(securityScheme.scopes) && scopeName in securityScheme.scopes;
+  }
+
+  if (isPlainObject(securityScheme.flows)) {
+    for (const flow of Object.values(securityScheme.flows)) {
+      if (isPlainObject(flow) && isPlainObject(flow.scopes) && scopeName in flow.scopes) {
+        return true;
+      }
+    }
+  }
+
+  return false;
+}


Test patch:
diff --git a/packages/rulesets/src/oas/__tests__/oas2-operation-security-defined.test.ts b/packages/rulesets/src/oas/__tests__/oas2-operation-security-defined.test.ts
index 92d714ac..3fd337d8 100644
--- a/packages/rulesets/src/oas/__tests__/oas2-operation-security-defined.test.ts
+++ b/packages/rulesets/src/oas/__tests__/oas2-operation-security-defined.test.ts
@@ -3,12 +3,21 @@ import testRule from './__helpers__/tester';
 
 testRule('oas2-operation-security-defined', [
   {
-    name: 'a correct object (just in body)',
+    name: 'valid case',
     document: {
       swagger: '2.0',
       securityDefinitions: {
-        apikey: {},
+        apikey: {
+          type: 'apiKey',
+          name: 'api_key',
+          in: 'header',
+        },
       },
+      security: [
+        {
+          apikey: [],
+        },
+      ],
       paths: {
         '/path': {
           get: {
@@ -25,37 +34,44 @@ testRule('oas2-operation-security-defined', [
   },
 
   {
-    name: 'a correct object (API-level security)',
+    name: 'valid and invalid object',
     document: {
       swagger: '2.0',
       securityDefinitions: {
-        apikey: {},
+        apikey: {
+          type: 'apiKey',
+          name: 'api_key',
+          in: 'header',
+        },
+        oauth2: {
+          type: 'oauth2',
+          flows: 'accessCode',
+          authorizationUrl: 'https://example.com/api/oauth/dialog',
+          tokenUrl: 'https://example.com/api/oauth/token',
+          scopes: {
+            'write:pets': 'modify pets in your account',
+            'read:pets': 'read your pets',
+          },
+        },
       },
       security: [
         {
           apikey: [],
+          basic: [],
+          oauth2: ['write:pets'],
         },
-      ],
-      paths: {
-        '/path': {
-          get: {},
+        {},
+        {
+          oauth2: ['write:users', 'read:users'],
         },
-      },
-    },
-    errors: [],
-  },
-
-  {
-    name: 'invalid object',
-    document: {
-      swagger: '2.0',
-      securityDefinitions: {},
+      ],
       paths: {
-        '/path': {
+        '/users': {
           get: {
             security: [
               {
-                apikey: [],
+                bearer: [],
+                oauth2: [],
               },
             ],
           },
@@ -64,45 +80,32 @@ testRule('oas2-operation-security-defined', [
     },
     errors: [
       {
-        message: 'Operation "security" values must match a scheme defined in the "securityDefinitions" object.',
-        path: ['paths', '/path', 'get', 'security', '0', 'apikey'],
+        message: 'API "security" values must match a scheme defined in the "securityDefinitions" object.',
+        path: ['security', '0', 'basic'],
         severity: DiagnosticSeverity.Warning,
       },
-    ],
-  },
-
-  {
-    name: 'invalid object (API-level security)',
-    document: {
-      swagger: '2.0',
-      securityDefinitions: {},
-      security: [
-        {
-          apikey: [],
-        },
-      ],
-      paths: {
-        '/path': {
-          get: {},
-        },
+      {
+        message: '"write:users" must be listed among scopes.',
+        path: ['security', '2', 'oauth2', '0'],
+        severity: DiagnosticSeverity.Warning,
       },
-    },
-    errors: [
       {
-        message: 'API "security" values must match a scheme defined in the "securityDefinitions" object.',
-        path: ['security', '0', 'apikey'],
+        message: '"read:users" must be listed among scopes.',
+        path: ['security', '2', 'oauth2', '1'],
+        severity: DiagnosticSeverity.Warning,
+      },
+      {
+        message: 'Operation "security" values must match a scheme defined in the "securityDefinitions" object.',
+        path: ['paths', '/users', 'get', 'security', '0', 'bearer'],
         severity: DiagnosticSeverity.Warning,
       },
     ],
   },
 
   {
-    name: 'valid and invalid object',
+    name: 'missing securityDefinitions',
     document: {
       swagger: '2.0',
-      securityDefinitions: {
-        apikey: {},
-      },
       paths: {
         '/path': {
           get: {
@@ -111,12 +114,18 @@ testRule('oas2-operation-security-defined', [
                 apikey: [],
                 basic: [],
               },
+              {},
             ],
           },
         },
       },
     },
     errors: [
+      {
+        message: 'Operation "security" values must match a scheme defined in the "securityDefinitions" object.',
+        path: ['paths', '/path', 'get', 'security', '0', 'apikey'],
+        severity: DiagnosticSeverity.Warning,
+      },
       {
         message: 'Operation "security" values must match a scheme defined in the "securityDefinitions" object.',
         path: ['paths', '/path', 'get', 'security', '0', 'basic'],
@@ -126,28 +135,58 @@ testRule('oas2-operation-security-defined', [
   },
 
   {
-    name: 'valid and invalid object (API-level security)',
+    name: 'invalid scopes in Security Scheme object',
     document: {
       swagger: '2.0',
       securityDefinitions: {
-        apikey: {},
-      },
-      security: [
-        {
-          apikey: [],
-          basic: [],
+        authorizationCode: {
+          type: 'oauth2',
+          flows: 'accessCode',
+          authorizationUrl: 'https://example.com/api/oauth/dialog',
+          tokenUrl: 'https://example.com/api/oauth/token',
+          scopes: null,
         },
-      ],
+        noFlows: {
+          type: 'oauth2',
+        },
+        client: {
+          type: 'oauth2',
+          flows: {
+            clientCredentials: null,
+          },
+        },
+      },
       paths: {
         '/path': {
-          get: {},
+          get: {
+            security: [
+              {
+                noFlows: ['read:users'],
+                authorizationCode: ['write:users'],
+              },
+              {
+                noFlows: [],
+                client: ['read:users'],
+              },
+            ],
+          },
         },
       },
     },
     errors: [
       {
-        message: 'API "security" values must match a scheme defined in the "securityDefinitions" object.',
-        path: ['security', '0', 'basic'],
+        message: '"read:users" must be listed among scopes.',
+        path: ['paths', '/path', 'get', 'security', '0', 'noFlows', '0'],
+        severity: DiagnosticSeverity.Warning,
+      },
+      {
+        message: '"write:users" must be listed among scopes.',
+        path: ['paths', '/path', 'get', 'security', '0', 'authorizationCode', '0'],
+        severity: DiagnosticSeverity.Warning,
+      },
+      {
+        message: '"read:users" must be listed among scopes.',
+        path: ['paths', '/path', 'get', 'security', '1', 'client', '0'],
         severity: DiagnosticSeverity.Warning,
       },
     ],
diff --git a/packages/rulesets/src/oas/__tests__/oas3-operation-security-defined.test.ts b/packages/rulesets/src/oas/__tests__/oas3-operation-security-defined.test.ts
index b024adc3..2046871f 100644
--- a/packages/rulesets/src/oas/__tests__/oas3-operation-security-defined.test.ts
+++ b/packages/rulesets/src/oas/__tests__/oas3-operation-security-defined.test.ts
@@ -3,14 +3,23 @@ import testRule from './__helpers__/tester';
 
 testRule('oas3-operation-security-defined', [
   {
-    name: 'validate a correct object (just in body)',
+    name: 'valid case',
     document: {
       openapi: '3.0.2',
       components: {
         securitySchemes: {
-          apikey: {},
+          apikey: {
+            type: 'apiKey',
+            name: 'api_key',
+            in: 'header',
+          },
         },
       },
+      security: [
+        {
+          apikey: [],
+        },
+      ],
       paths: {
         '/path': {
           get: {
@@ -25,40 +34,51 @@ testRule('oas3-operation-security-defined', [
     },
     errors: [],
   },
+
   {
-    name: 'validate a correct object (API-level security)',
+    name: 'valid and invalid object',
     document: {
       openapi: '3.0.2',
       components: {
         securitySchemes: {
-          apikey: {},
-        },
-        security: [
-          {
-            apikey: [],
+          apikey: {
+            type: 'apiKey',
+            name: 'api_key',
+            in: 'header',
+          },
+          oauth2: {
+            type: 'oauth2',
+            flows: {
+              authorizationCode: {
+                authorizationUrl: 'https://example.com/api/oauth/dialog',
+                tokenUrl: 'https://example.com/api/oauth/token',
+                scopes: {
+                  'write:pets': 'modify pets in your account',
+                  'read:pets': 'read your pets',
+                },
+              },
+            },
           },
-        ],
-      },
-      paths: {
-        '/path': {
-          get: {},
         },
       },
-    },
-    errors: [],
-  },
-
-  {
-    name: 'return errors on invalid object',
-    document: {
-      openapi: '3.0.2',
-      components: {},
+      security: [
+        {
+          apikey: [],
+          basic: [],
+          oauth2: ['write:pets'],
+        },
+        {},
+        {
+          oauth2: ['write:users', 'read:users'],
+        },
+      ],
       paths: {
-        '/path': {
+        '/users': {
           get: {
             security: [
               {
-                apikey: [],
+                bearer: [],
+                oauth2: [],
               },
             ],
           },
@@ -67,47 +87,32 @@ testRule('oas3-operation-security-defined', [
     },
     errors: [
       {
-        message: 'Operation "security" values must match a scheme defined in the "components.securitySchemes" object.',
-        path: ['paths', '/path', 'get', 'security', '0', 'apikey'],
+        message: 'API "security" values must match a scheme defined in the "components.securitySchemes" object.',
+        path: ['security', '0', 'basic'],
         severity: DiagnosticSeverity.Warning,
       },
-    ],
-  },
-
-  {
-    name: 'return errors on invalid object (API-level)',
-    document: {
-      openapi: '3.0.2',
-      components: {},
-      security: [
-        {
-          apikey: [],
-        },
-      ],
-      paths: {
-        '/path': {
-          get: {},
-        },
+      {
+        message: '"write:users" must be listed among scopes.',
+        path: ['security', '2', 'oauth2', '0'],
+        severity: DiagnosticSeverity.Warning,
       },
-    },
-    errors: [
       {
-        message: 'API "security" values must match a scheme defined in the "components.securitySchemes" object.',
-        path: ['security', '0', 'apikey'],
+        message: '"read:users" must be listed among scopes.',
+        path: ['security', '2', 'oauth2', '1'],
+        severity: DiagnosticSeverity.Warning,
+      },
+      {
+        message: 'Operation "security" values must match a scheme defined in the "components.securitySchemes" object.',
+        path: ['paths', '/users', 'get', 'security', '0', 'bearer'],
         severity: DiagnosticSeverity.Warning,
       },
     ],
   },
 
   {
-    name: 'return errors on valid and invalid object',
+    name: 'missing securitySchemes',
     document: {
-      openapi: '3.0.2',
-      components: {
-        securitySchemes: {
-          apikey: {},
-        },
-      },
+      openapi: '3.1.0',
       paths: {
         '/path': {
           get: {
@@ -123,6 +128,11 @@ testRule('oas3-operation-security-defined', [
       },
     },
     errors: [
+      {
+        message: 'Operation "security" values must match a scheme defined in the "components.securitySchemes" object.',
+        path: ['paths', '/path', 'get', 'security', '0', 'apikey'],
+        severity: DiagnosticSeverity.Warning,
+      },
       {
         message: 'Operation "security" values must match a scheme defined in the "components.securitySchemes" object.',
         path: ['paths', '/path', 'get', 'security', '0', 'basic'],
@@ -132,30 +142,70 @@ testRule('oas3-operation-security-defined', [
   },
 
   {
-    name: 'valid and invalid object (API-level security)',
+    name: 'invalid scopes in Security Scheme object',
     document: {
-      openapi: '3.0.2',
+      openapi: '3.1.0',
       components: {
         securitySchemes: {
-          apikey: {},
+          authorizationCode: {
+            type: 'oauth2',
+            flows: {
+              authorizationCode: {
+                authorizationUrl: 'https://example.com/api/oauth/dialog',
+                tokenUrl: 'https://example.com/api/oauth/token',
+                scopes: null,
+              },
+            },
+          },
+          noFlows: {
+            type: 'oauth2',
+          },
+          client: {
+            type: 'oauth2',
+            flows: {
+              clientCredentials: null,
+            },
+          },
+          broken: null,
         },
       },
-      security: [
-        {
-          apikey: [],
-          basic: [],
-        },
-      ],
       paths: {
         '/path': {
-          get: {},
+          get: {
+            security: [
+              {
+                noFlows: ['read:users'],
+                authorizationCode: ['write:users'],
+                broken: ['delete:users'],
+              },
+              {
+                noFlows: [],
+                client: ['read:users'],
+              },
+            ],
+          },
         },
       },
     },
     errors: [
       {
-        message: 'API "security" values must match a scheme defined in the "components.securitySchemes" object.',
-        path: ['security', '0', 'basic'],
+        message: '"read:users" must be listed among scopes.',
+        path: ['paths', '/path', 'get', 'security', '0', 'noFlows', '0'],
+        severity: DiagnosticSeverity.Warning,
+      },
+      {
+        message: '"write:users" must be listed among scopes.',
+        path: ['paths', '/path', 'get', 'security', '0', 'authorizationCode', '0'],
+        severity: DiagnosticSeverity.Warning,
+      },
+      {
+        message: '"delete:users" must be listed among scopes.',
+        path: ['paths', '/path', 'get', 'security', '0', 'broken', '0'],
+        severity: DiagnosticSeverity.Warning,
+      },
+      {
+        message: '"read:users" must be listed among scopes.',
+        path: ['paths', '/path', 'get', 'security', '1', 'client', '0'],
         severity: DiagnosticSeverity.Warning,
       },
     ],