mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
267 lines
7.3 KiB
TypeScript
267 lines
7.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import openaiSchema from "./fixtures/openai-credential-schema.json";
|
|
import templateSchema from "./fixtures/template-credential-schema.json";
|
|
import {
|
|
parseRegistryCredentialSchema,
|
|
REGISTRY_CREDENTIAL_SCHEMA_LIMITS,
|
|
} from "./provider-credential-schema";
|
|
|
|
const schema = {
|
|
type: "object",
|
|
properties: {
|
|
api_key: {
|
|
title: "API Key",
|
|
description: "The key.",
|
|
type: "string",
|
|
format: "password",
|
|
writeOnly: true,
|
|
},
|
|
scheme: {
|
|
title: "Scheme",
|
|
type: "string",
|
|
enum: ["bearer", "basic"],
|
|
default: "bearer",
|
|
},
|
|
notes: {
|
|
title: "Notes",
|
|
type: "string",
|
|
"x-prowler-widget": "textarea",
|
|
default: "",
|
|
},
|
|
},
|
|
required: ["api_key"],
|
|
};
|
|
|
|
describe("parseRegistryCredentialSchema", () => {
|
|
it("accepts the installed Template 0.2.5 schema with typed fields and examples", () => {
|
|
// Given / When
|
|
const result = parseRegistryCredentialSchema(templateSchema);
|
|
|
|
// Then
|
|
expect(result?.fields.map(({ name, kind }) => [name, kind])).toEqual([
|
|
["api_url", "text"],
|
|
["api_key", "password"],
|
|
["ca_bundle", "textarea"],
|
|
["verify_tls", "checkbox"],
|
|
["timeout_seconds", "integer"],
|
|
["auth_scheme", "select"],
|
|
]);
|
|
expect(result?.fields[0]).toMatchObject({
|
|
placeholder: "https://api.acme.com",
|
|
required: true,
|
|
});
|
|
expect(result?.fields[3].defaultValue).toBe(true);
|
|
expect(result?.fields[4]).toMatchObject({
|
|
defaultValue: 30,
|
|
minimum: 1,
|
|
maximum: 300,
|
|
});
|
|
});
|
|
it.each([
|
|
"api_key",
|
|
"platform_api_key",
|
|
"platform-api-key",
|
|
"apiKey",
|
|
"platformApiKey",
|
|
"platformAPIKey",
|
|
"API_KEY",
|
|
"apikey",
|
|
])("masks the plain API key field %s without schema annotations", (name) => {
|
|
// Given / When
|
|
const result = parseRegistryCredentialSchema({
|
|
type: "object",
|
|
properties: { [name]: { type: "string" } },
|
|
});
|
|
|
|
// Then
|
|
expect(result?.fields[0].kind).toBe("password");
|
|
});
|
|
|
|
it("keeps identifiers and explicitly configured widgets unchanged", () => {
|
|
// Given / When
|
|
const result = parseRegistryCredentialSchema({
|
|
type: "object",
|
|
properties: {
|
|
api_key_id: { type: "string" },
|
|
api_key_url: { type: "string" },
|
|
selected_api_key: { type: "string", enum: ["primary", "secondary"] },
|
|
multiline_api_key: { type: "string", "x-prowler-widget": "textarea" },
|
|
},
|
|
});
|
|
|
|
// Then
|
|
expect(result?.fields.map(({ kind }) => kind)).toEqual([
|
|
"text",
|
|
"text",
|
|
"select",
|
|
"textarea",
|
|
]);
|
|
});
|
|
|
|
it("accepts the installed OpenAI schema with its full description", () => {
|
|
// Given / When: the materialized OpenAI 0.1.5 schema contains a long docstring.
|
|
const result = parseRegistryCredentialSchema(openaiSchema);
|
|
|
|
// Then
|
|
expect(result?.fields.map(({ name }) => name)).toEqual([
|
|
"organization_id",
|
|
"platform_api_key",
|
|
"base_url",
|
|
]);
|
|
expect(result?.fields[2].defaultValue).toBe("https://api.openai.com/v1");
|
|
expect(result?.fields.map(({ kind }) => kind)).toEqual([
|
|
"text",
|
|
"password",
|
|
"text",
|
|
]);
|
|
});
|
|
|
|
it("preserves long field descriptions without treating them as input limits", () => {
|
|
// Given
|
|
const description = openaiSchema.description;
|
|
|
|
// When
|
|
const result = parseRegistryCredentialSchema({
|
|
...schema,
|
|
properties: { token: { type: "string", description } },
|
|
required: ["token"],
|
|
});
|
|
|
|
// Then
|
|
expect(result?.fields[0].description).toBe(description);
|
|
});
|
|
|
|
it("accepts the observed flat credential schema and preserves property order", () => {
|
|
// Given
|
|
const result = parseRegistryCredentialSchema(schema);
|
|
|
|
// When / Then
|
|
|
|
expect(result?.fields.map(({ name, kind }) => [name, kind])).toEqual([
|
|
["api_key", "password"],
|
|
["scheme", "select"],
|
|
["notes", "textarea"],
|
|
]);
|
|
|
|
expect(result?.fields[0]).toMatchObject({
|
|
description: "The key.",
|
|
label: "API Key",
|
|
required: true,
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
["$ref", { $ref: "#/$defs/credential" }],
|
|
["$defs", { $defs: {} }],
|
|
["definitions", { definitions: {} }],
|
|
["combinators", { anyOf: [] }],
|
|
["additional properties", { additionalProperties: true }],
|
|
])("rejects risky root keywords: %s", (_name, keyword) => {
|
|
expect(parseRegistryCredentialSchema({ ...schema, ...keyword })).toBeNull();
|
|
});
|
|
|
|
it.each([
|
|
["nested objects", { type: "object", properties: {} }],
|
|
["arrays", { type: "array" }],
|
|
["nullable unions", { type: ["string", "null"] }],
|
|
["unsupported formats", { type: "string", format: "email" }],
|
|
["passwords without writeOnly", { type: "string", format: "password" }],
|
|
["maps", { type: "string", additionalProperties: true }],
|
|
])("rejects unsupported fields: %s", (_name, apiKey) => {
|
|
expect(
|
|
parseRegistryCredentialSchema({
|
|
...schema,
|
|
properties: { ...schema.properties, api_key: apiKey },
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
it.each([
|
|
{ type: "boolean", default: "true" },
|
|
{ type: "boolean", enum: [true] },
|
|
{ type: "integer", default: "30" },
|
|
{ type: "integer", default: 1.5 },
|
|
{ type: "integer", minimum: 1, default: 0 },
|
|
{ type: "integer", maximum: 300, default: 301 },
|
|
{ type: "integer", minimum: 10, maximum: 1 },
|
|
{ type: "integer", minimum: "1" },
|
|
{ type: "integer", maximum: Infinity },
|
|
{ type: "integer", multipleOf: 5 },
|
|
{ type: "string", examples: "not-an-array" },
|
|
{ type: "string", examples: [{ value: "unexpected" }] },
|
|
])(
|
|
"rejects malformed annotations or unsupported constraints: %j",
|
|
(property) => {
|
|
expect(
|
|
parseRegistryCredentialSchema({
|
|
type: "object",
|
|
properties: { field: property },
|
|
}),
|
|
).toBeNull();
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
[
|
|
"invalid defaults",
|
|
{ type: "string", enum: ["bearer", "basic"], default: "token" },
|
|
],
|
|
["duplicate values", { type: "string", enum: ["bearer", "bearer"] }],
|
|
])("rejects enum definitions with %s", (_name, scheme) => {
|
|
expect(
|
|
parseRegistryCredentialSchema({
|
|
...schema,
|
|
properties: { ...schema.properties, scheme },
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("rejects unsafe names, invalid required fields, and over-limit metadata", () => {
|
|
// Given
|
|
|
|
const fields = Object.fromEntries(
|
|
Array.from(
|
|
{ length: REGISTRY_CREDENTIAL_SCHEMA_LIMITS.MAX_FIELDS + 1 },
|
|
(_, index) => [`field${index}`, { type: "string" }],
|
|
),
|
|
);
|
|
|
|
const cases = [
|
|
{ ...schema, required: ["missing"] },
|
|
{ ...schema, description: { invalid: "not text" } },
|
|
{
|
|
...schema,
|
|
properties: { api_key: { type: "string", description: 123 } },
|
|
},
|
|
JSON.parse(
|
|
'{"type":"object","properties":{"__proto__":{"type":"string"}}}',
|
|
),
|
|
{ type: "object", properties: fields },
|
|
{
|
|
...schema,
|
|
properties: {
|
|
...schema.properties,
|
|
notes: {
|
|
...schema.properties.notes,
|
|
title: "a".repeat(
|
|
REGISTRY_CREDENTIAL_SCHEMA_LIMITS.MAX_TEXT_LENGTH + 1,
|
|
),
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
// When / Then
|
|
|
|
expect(cases.map(parseRegistryCredentialSchema)).toEqual([
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
]);
|
|
});
|
|
});
|