lint: share sensitive url config rules

This commit is contained in:
Mason Huang
2026-04-18 17:47:28 +08:00
parent 47b8a4056b
commit 7e738f4f5c
3 changed files with 26 additions and 59 deletions

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
import { promises as fs } from "node:fs";
/**
* Lint: detect schema URL config fields that are missing redact coverage.
*
@@ -14,12 +15,11 @@
*
* That prevents regressions like the browser.cdpUrl omission from PR #67679.
*
* The isSensitiveUrlConfigPath rules are extracted from source instead of being
* duplicated manually, so this lint always matches the real runtime behavior.
* The sensitive-path rules come from the same shared JSON data used by runtime
* code, so this lint stays aligned with real behavior without parsing source.
*/
import { promises as fs } from "node:fs";
import path from "node:path";
import sensitiveUrlConfigRules from "../src/shared/net/sensitive-url-config-rules.json" with { type: "json" };
// These URL-shaped field names do not carry credentials and do not need redaction.
const SAFE_URL_PATTERNS = [
@@ -33,50 +33,19 @@ function isSafeUrlField(key) {
return SAFE_URL_PATTERNS.some((p) => p.test(key));
}
/**
* Extract path suffix rules for isSensitiveUrlConfigPath() directly from
* redact-sensitive-url.ts by parsing endsWith(".xxx") calls.
*/
function extractEndsWithRules(source) {
const rules = [];
// Match path.endsWith(".xxx") patterns.
const endsWithPattern = /path\.endsWith\("(\.[^"]+)"\)/g;
let match;
while ((match = endsWithPattern.exec(source)) !== null) {
rules.push(match[1]);
}
return rules;
}
/**
* Extract regex rules directly from redact-sensitive-url.ts by parsing
* /^...$/ regex literals used against config paths.
*/
function extractRegexRules(source) {
const rules = [];
// Match /pattern/.test(path) patterns.
const regexPattern = /\/(\^[^/]+)\$\/\.test\(path\)/g;
let match;
while ((match = regexPattern.exec(source)) !== null) {
rules.push(new RegExp(match[1] + "$"));
}
return rules;
}
/**
* Build an isSensitiveUrlConfigPath equivalent from rules extracted from source.
*/
function buildIsSensitiveUrlConfigPath(source) {
const endsWithRules = extractEndsWithRules(source);
const regexRules = extractRegexRules(source);
const SENSITIVE_URL_CONFIG_SUFFIXES = sensitiveUrlConfigRules.suffixes;
const SENSITIVE_URL_CONFIG_PATTERNS = sensitiveUrlConfigRules.patterns.map(
(pattern) => new RegExp(pattern),
);
function buildIsSensitiveUrlConfigPath() {
return function isSensitiveUrlConfigPath(configPath) {
for (const suffix of endsWithRules) {
for (const suffix of SENSITIVE_URL_CONFIG_SUFFIXES) {
if (configPath.endsWith(suffix)) {
return true;
}
}
for (const regex of regexRules) {
for (const regex of SENSITIVE_URL_CONFIG_PATTERNS) {
if (regex.test(configPath)) {
return true;
}
@@ -87,11 +56,7 @@ function buildIsSensitiveUrlConfigPath(source) {
async function run() {
const repoRoot = path.resolve(import.meta.dirname, "..");
// Read redact-sensitive-url.ts and extract the sensitive-path rules.
const redactSourcePath = path.join(repoRoot, "src/shared/net/redact-sensitive-url.ts");
const redactSource = await fs.readFile(redactSourcePath, "utf8");
const isSensitiveUrlConfigPath = buildIsSensitiveUrlConfigPath(redactSource);
const isSensitiveUrlConfigPath = buildIsSensitiveUrlConfigPath();
// Read schema.base.generated.ts and inspect its URL-shaped config fields.
const schemaPath = path.join(repoRoot, "src/config/schema.base.generated.ts");

View File

@@ -1,7 +1,12 @@
import type { ConfigUiHint } from "../config-ui-hints-types.js";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.js";
import sensitiveUrlConfigRules from "./sensitive-url-config-rules.json" with { type: "json" };
export const SENSITIVE_URL_HINT_TAG = "url-secret";
const SENSITIVE_URL_CONFIG_SUFFIXES = sensitiveUrlConfigRules.suffixes;
const SENSITIVE_URL_CONFIG_PATTERNS = sensitiveUrlConfigRules.patterns.map(
(pattern) => new RegExp(pattern),
);
const SENSITIVE_URL_QUERY_PARAM_NAMES = new Set([
"token",
@@ -22,19 +27,12 @@ export function isSensitiveUrlQueryParamName(name: string): boolean {
}
export function isSensitiveUrlConfigPath(path: string): boolean {
if (path.endsWith(".baseUrl") || path.endsWith(".httpUrl")) {
return true;
for (const suffix of SENSITIVE_URL_CONFIG_SUFFIXES) {
if (path.endsWith(suffix)) {
return true;
}
}
if (path.endsWith(".cdpUrl")) {
return true;
}
if (path.endsWith(".remote.url")) {
return true;
}
if (path.endsWith(".request.proxy.url")) {
return true;
}
return /^mcp\.servers\.(?:\*|[^.]+)\.url$/.test(path);
return SENSITIVE_URL_CONFIG_PATTERNS.some((pattern) => pattern.test(path));
}
export function hasSensitiveUrlHintTag(hint: Pick<ConfigUiHint, "tags"> | undefined): boolean {

View File

@@ -0,0 +1,4 @@
{
"suffixes": [".baseUrl", ".httpUrl", ".cdpUrl", ".remote.url", ".request.proxy.url"],
"patterns": ["^mcp\\.servers\\.(?:\\*|[^.]+)\\.url$"]
}