Implements a comprehensive suite of AI SDK v6 tools across multiple categories: - Research (5): page-brief, compare-pages, source-credibility, claim-checklist, timeline-from-text - Web (10): fetch-text, links-catalog, extract-meta, extract-json-ld, redirect-trace, sitemap-read, rss-read, table-extract, robots-policy, url-normalize - Data (15): csv-parse, csv-stringify, json-repair, json-schema-validate, yaml-parse, yaml-stringify, text-chunk, normalize-whitespace, dedupe-by-key, pivot, rows-filter, rows-sort, rows-group-aggregate, rows-join, schema-infer - Doc (12): toc-generate, glossary-build, faq-from-text, executive-brief, decision-record-adr, prd-outline, acceptance-criteria, style-rewrite - Eng (12): diff-text-unified, env-var-docs-generate, dependency-audit-lite, conventional-commit-suggest, markdown-lint-basic, test-case-generate, stacktrace-parse, release-notes, changelog-entry, release-checklist - Security (7): redact-secrets, secret-scan-text, url-risk-heuristic, csp-compose, hardening-checklist-web, access-control-matrix, data-classification-heuristic - Stats (9): effect-size-suite, bootstrap-ci, permutation-test, multiple-testing-adjust, linear-regression-ols, logistic-regression, time-series-decompose-lite, anomaly-detect-mad - Ops (7): slo-draft, runbook-draft, postmortem-draft, postmortem-action-extractor, error-log-triage, coverage-tracker, monitoring-gap-analysis - Agent (15): prompt-to-workflow-skeleton, workflow-validate-io, workflow-explain, workflow-cost-estimate, tool-call-accuracy-score, eval-fixture-build, guardrail-policy-draft, workflow-auto-repair, tool-selection-plan, novelty-score-workflow, workflow-variant-generate, config-normalize, recipe-* - Utility (8): base64-encode, base64-decode, hash-text, regex-extract, template-render, date-parse, json-path-query, url-parse - HTML (3): html-sanitize, html-to-markdown, markdown-to-html All tools follow AI SDK v6 pattern with tool() and jsonSchema<T>(). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
258 lines
7.2 KiB
TypeScript
258 lines
7.2 KiB
TypeScript
/**
|
|
* CSP Compose Tool for TPMJS
|
|
* Composes Content Security Policy headers from directive configurations.
|
|
* Validates directives and checks for strict CSP patterns.
|
|
*/
|
|
|
|
import { jsonSchema, tool } from 'ai';
|
|
|
|
/**
|
|
* Output interface for CSP composition
|
|
*/
|
|
export interface CSPResult {
|
|
header: string;
|
|
directives: Array<{
|
|
directive: string;
|
|
sources: string[];
|
|
}>;
|
|
isStrict: boolean;
|
|
warnings: string[];
|
|
}
|
|
|
|
type CSPComposeInput = {
|
|
policies: Record<string, string[]>;
|
|
};
|
|
|
|
/**
|
|
* Valid CSP directive names
|
|
*/
|
|
const VALID_DIRECTIVES = new Set([
|
|
'default-src',
|
|
'script-src',
|
|
'style-src',
|
|
'img-src',
|
|
'font-src',
|
|
'connect-src',
|
|
'media-src',
|
|
'object-src',
|
|
'frame-src',
|
|
'child-src',
|
|
'worker-src',
|
|
'manifest-src',
|
|
'base-uri',
|
|
'form-action',
|
|
'frame-ancestors',
|
|
'report-uri',
|
|
'report-to',
|
|
'upgrade-insecure-requests',
|
|
'block-all-mixed-content',
|
|
]);
|
|
|
|
/**
|
|
* Unsafe CSP sources that weaken security
|
|
*/
|
|
const UNSAFE_SOURCES = new Set(["'unsafe-inline'", "'unsafe-eval'", "'unsafe-hashes'"]);
|
|
|
|
/**
|
|
* Validates a CSP directive name
|
|
*/
|
|
function isValidDirective(directive: string): boolean {
|
|
return VALID_DIRECTIVES.has(directive);
|
|
}
|
|
|
|
/**
|
|
* Checks if a policy is considered strict
|
|
* A strict CSP:
|
|
* - Uses nonces or hashes for scripts
|
|
* - Avoids 'unsafe-inline' and 'unsafe-eval'
|
|
* - Has a restrictive default-src
|
|
*/
|
|
function isStrictCSP(policies: Record<string, string[]>): boolean {
|
|
// Check for unsafe sources in critical directives
|
|
const criticalDirectives = ['default-src', 'script-src', 'style-src'];
|
|
|
|
for (const directive of criticalDirectives) {
|
|
const sources = policies[directive] || [];
|
|
for (const source of sources) {
|
|
if (UNSAFE_SOURCES.has(source)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if script-src uses nonces or hashes
|
|
const scriptSrc = policies['script-src'] || policies['default-src'] || [];
|
|
const hasNonceOrHash = scriptSrc.some(
|
|
(source) => source.startsWith("'nonce-") || source.startsWith("'sha")
|
|
);
|
|
|
|
// Check for restrictive default-src
|
|
const defaultSrc = policies['default-src'] || [];
|
|
const hasRestrictiveDefault = defaultSrc.includes("'self'") || defaultSrc.includes("'none'");
|
|
|
|
return hasNonceOrHash && hasRestrictiveDefault;
|
|
}
|
|
|
|
/**
|
|
* Generates warnings for common CSP issues
|
|
*/
|
|
function generateWarnings(policies: Record<string, string[]>): string[] {
|
|
const warnings: string[] = [];
|
|
|
|
// Check for unsafe-inline
|
|
for (const [directive, sources] of Object.entries(policies)) {
|
|
if (sources.includes("'unsafe-inline'")) {
|
|
warnings.push(
|
|
`${directive} contains 'unsafe-inline' which allows inline scripts/styles and weakens CSP protection`
|
|
);
|
|
}
|
|
if (sources.includes("'unsafe-eval'")) {
|
|
warnings.push(
|
|
`${directive} contains 'unsafe-eval' which allows eval() and similar functions, creating XSS risks`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Check for wildcard sources
|
|
for (const [directive, sources] of Object.entries(policies)) {
|
|
if (sources.includes('*')) {
|
|
warnings.push(`${directive} contains wildcard (*) which allows resources from any origin`);
|
|
}
|
|
if (sources.some((s) => s.startsWith('*.'))) {
|
|
warnings.push(
|
|
`${directive} contains subdomain wildcard (*.domain) which may be overly permissive`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Check if default-src is missing
|
|
if (!policies['default-src']) {
|
|
warnings.push(
|
|
"Missing 'default-src' directive - consider adding a restrictive default fallback"
|
|
);
|
|
}
|
|
|
|
// Check for missing object-src
|
|
if (!policies['object-src']) {
|
|
warnings.push(
|
|
"Missing 'object-src' directive - consider adding \"object-src 'none'\" to block plugins"
|
|
);
|
|
}
|
|
|
|
// Check for missing base-uri
|
|
if (!policies['base-uri']) {
|
|
warnings.push(
|
|
"Missing 'base-uri' directive - consider adding \"base-uri 'self'\" to prevent base tag injection"
|
|
);
|
|
}
|
|
|
|
return warnings;
|
|
}
|
|
|
|
/**
|
|
* Formats sources for a directive
|
|
*/
|
|
function formatSources(sources: string[]): string {
|
|
// Remove duplicates and sort
|
|
const uniqueSources = Array.from(new Set(sources));
|
|
return uniqueSources.join(' ');
|
|
}
|
|
|
|
/**
|
|
* CSP Compose Tool
|
|
* Composes a Content Security Policy header from directive configurations
|
|
*/
|
|
export const cspComposeTool = tool({
|
|
description:
|
|
'Compose a Content Security Policy (CSP) header from directive configurations. Validates directives, checks for security issues, and determines if the policy is strict. Returns the formatted CSP header string, directive details, and security warnings.',
|
|
inputSchema: jsonSchema<CSPComposeInput>({
|
|
type: 'object',
|
|
properties: {
|
|
policies: {
|
|
type: 'object',
|
|
description:
|
|
'CSP directives mapped to arrays of source values. Example: { "default-src": ["\'self\'"], "script-src": ["\'nonce-abc123\'", "https://cdn.example.com"] }',
|
|
additionalProperties: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'string',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
required: ['policies'],
|
|
additionalProperties: false,
|
|
}),
|
|
async execute({ policies }): Promise<CSPResult> {
|
|
// Validate input
|
|
if (!policies || typeof policies !== 'object') {
|
|
throw new Error('Policies must be an object mapping directives to source arrays');
|
|
}
|
|
|
|
if (Object.keys(policies).length === 0) {
|
|
throw new Error('At least one CSP directive is required');
|
|
}
|
|
|
|
// Validate and build directives
|
|
const directives: Array<{ directive: string; sources: string[] }> = [];
|
|
const headerParts: string[] = [];
|
|
|
|
for (const [directive, sources] of Object.entries(policies)) {
|
|
// Validate directive name
|
|
if (!isValidDirective(directive)) {
|
|
throw new Error(
|
|
`Invalid CSP directive: "${directive}". Must be one of: ${Array.from(VALID_DIRECTIVES).join(', ')}`
|
|
);
|
|
}
|
|
|
|
// Validate sources is an array
|
|
if (!Array.isArray(sources)) {
|
|
throw new Error(`Sources for directive "${directive}" must be an array`);
|
|
}
|
|
|
|
// Handle directives without values (flags)
|
|
if (directive === 'upgrade-insecure-requests' || directive === 'block-all-mixed-content') {
|
|
directives.push({ directive, sources: [] });
|
|
headerParts.push(directive);
|
|
continue;
|
|
}
|
|
|
|
// Validate sources is not empty for value directives
|
|
if (sources.length === 0) {
|
|
throw new Error(`Directive "${directive}" requires at least one source value`);
|
|
}
|
|
|
|
// Validate each source
|
|
for (const source of sources) {
|
|
if (typeof source !== 'string' || source.trim().length === 0) {
|
|
throw new Error(
|
|
`Invalid source value in "${directive}": sources must be non-empty strings`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Build directive string
|
|
const formattedSources = formatSources(sources);
|
|
directives.push({ directive, sources: [...sources] });
|
|
headerParts.push(`${directive} ${formattedSources}`);
|
|
}
|
|
|
|
// Build the final header
|
|
const header = headerParts.join('; ');
|
|
|
|
// Check if the policy is strict
|
|
const isStrict = isStrictCSP(policies);
|
|
|
|
// Generate warnings
|
|
const warnings = generateWarnings(policies);
|
|
|
|
return {
|
|
header,
|
|
directives,
|
|
isStrict,
|
|
warnings,
|
|
};
|
|
},
|
|
});
|
|
|
|
export default cspComposeTool;
|