Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -63,28 +63,29 @@ GITTENSORY_REVIEW_ENRICHMENT=false
# # Unknown names warn and are ignored; a typo-only list runs no analyzers.
# BEGIN GENERATED REES ANALYZERS
# Current analyzer names:
# dependency,lockfileDrift,secret,license,installScript,heavyDependency,actionPin,eol,redos
# provenance,codeowners,secretLog,assetWeight,typosquat,commitSignature,iacMisconfig,nativeBuild
# history,docCommentDrift,duplication,churnHotspot,blameLink,approvalIntegrity,ciCheckSignals
# undocumentedExport,staleBranch,commitHygiene,pendingReviewRequests,testRatio,migrationSafety
# looseRange,terminology,todoMarker,magicNumber,conflictMarker,debugLeftover,commitLint
# dependency,lockfileDrift,secret,license,installScript,heavyDependency,hardcodedUrl,actionPin
# eol,redos,provenance,codeowners,secretLog,assetWeight,typosquat,commitSignature,iacMisconfig
# nativeBuild,history,docCommentDrift,duplication,churnHotspot,blameLink,approvalIntegrity
# ciCheckSignals,undocumentedExport,staleBranch,commitHygiene,pendingReviewRequests,testRatio
# migrationSafety,looseRange,terminology,todoMarker,magicNumber,conflictMarker,debugLeftover
# commitLint
#
# Profile defaults:
# fast: dependency,lockfileDrift,secret,license,installScript,heavyDependency,actionPin,eol
# redos,provenance,secretLog,typosquat,iacMisconfig,nativeBuild,testRatio,migrationSafety
# looseRange,terminology,todoMarker,magicNumber,conflictMarker,debugLeftover
# fast: dependency,lockfileDrift,secret,license,installScript,heavyDependency,hardcodedUrl
# actionPin,eol,redos,provenance,secretLog,typosquat,iacMisconfig,nativeBuild,testRatio
# migrationSafety,looseRange,terminology,todoMarker,magicNumber,conflictMarker,debugLeftover
# balanced (default): dependency,lockfileDrift,secret,license,installScript,heavyDependency
# hardcodedUrl,actionPin,eol,redos,provenance,codeowners,secretLog,assetWeight,typosquat
# commitSignature,iacMisconfig,nativeBuild,history,docCommentDrift,duplication,churnHotspot
# blameLink,approvalIntegrity,ciCheckSignals,undocumentedExport,staleBranch,commitHygiene
# pendingReviewRequests,testRatio,migrationSafety,looseRange,terminology,todoMarker,magicNumber
# conflictMarker,debugLeftover,commitLint
# deep: dependency,lockfileDrift,secret,license,installScript,heavyDependency,hardcodedUrl
# actionPin,eol,redos,provenance,codeowners,secretLog,assetWeight,typosquat,commitSignature
# iacMisconfig,nativeBuild,history,docCommentDrift,duplication,churnHotspot,blameLink
# approvalIntegrity,ciCheckSignals,undocumentedExport,staleBranch,commitHygiene
# pendingReviewRequests,testRatio,migrationSafety,looseRange,terminology,todoMarker,magicNumber
# conflictMarker,debugLeftover,commitLint
# deep: dependency,lockfileDrift,secret,license,installScript,heavyDependency,actionPin,eol
# redos,provenance,codeowners,secretLog,assetWeight,typosquat,commitSignature,iacMisconfig
# nativeBuild,history,docCommentDrift,duplication,churnHotspot,blameLink,approvalIntegrity
# ciCheckSignals,undocumentedExport,staleBranch,commitHygiene,pendingReviewRequests,testRatio
# migrationSafety,looseRange,terminology,todoMarker,magicNumber,conflictMarker,debugLeftover
# commitLint
# END GENERATED REES ANALYZERS

# Submitter-reputation spend control (internal-only): downgrades new/burst/low-rep
Expand Down
25 changes: 25 additions & 0 deletions apps/gittensory-ui/src/lib/rees-analyzers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,31 @@ export const REES_ANALYZERS = [
notes: "Only reports packages with trivial direct usage so the finding stays actionable.",
},
},
{
name: "hardcodedUrl",
title: "Hardcoded URLs and endpoints",
category: "config",
cost: "local",
defaultEnabled: true,
profiles: ["fast", "balanced", "deep"],
requires: ["files"],
limits: {
maxFindings: 25,
maxLineChars: 2000,
maxHostChars: 40,
},
docs: {
summary:
"Flags absolute HTTP(S) URLs and raw IP:port endpoints newly added in non-test, non-config source.",
looksAt:
"Added lines in changed source files, excluding tests, config manifests, and comment/import lines.",
reports:
"File, line, kind (http-url or ip-endpoint), and a redacted host — never full paths or queries.",
network: "Pure local analyzer. No external network call.",
notes:
"localhost, 127.0.0.1, and example.com are allowlisted. Distinct from the secret scanner — no credential detection.",
},
},
{
name: "actionPin",
title: "Unpinned GitHub Actions",
Expand Down
27 changes: 27 additions & 0 deletions review-enrichment/analyzer-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,33 @@
"notes": "Only reports packages with trivial direct usage so the finding stays actionable."
}
},
{
"name": "hardcodedUrl",
"title": "Hardcoded URLs and endpoints",
"category": "config",
"cost": "local",
"defaultEnabled": true,
"profiles": [
"fast",
"balanced",
"deep"
],
"requires": [
"files"
],
"limits": {
"maxFindings": 25,
"maxLineChars": 2000,
"maxHostChars": 40
},
"docs": {
"summary": "Flags absolute HTTP(S) URLs and raw IP:port endpoints newly added in non-test, non-config source.",
"looksAt": "Added lines in changed source files, excluding tests, config manifests, and comment/import lines.",
"reports": "File, line, kind (http-url or ip-endpoint), and a redacted host — never full paths or queries.",
"network": "Pure local analyzer. No external network call.",
"notes": "localhost, 127.0.0.1, and example.com are allowlisted. Distinct from the secret scanner — no credential detection."
}
},
{
"name": "actionPin",
"title": "Unpinned GitHub Actions",
Expand Down
143 changes: 143 additions & 0 deletions review-enrichment/src/analyzers/hardcoded-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Hardcoded-URL / raw-endpoint analyzer (#2027). Flags absolute HTTP(S) URLs and IP:port endpoints newly
// added in non-test, non-config source — often environment leakage or a value that should come from config.
// Distinct from the secret scanner (no credential); this is a portability/config-hygiene signal. Pure compute
// over added lines, no network. Hostnames are redacted/truncated in findings — never full paths or queries.
import type { EnrichRequest, HardcodedUrlFinding } from "../types.js";
import { isMagicNumberSourcePath } from "./magic-number.js";

const MAX_FINDINGS = 25;
const MAX_LINE_CHARS = 2000;
const MAX_HOST_CHARS = 40;

const CONFIG_PATH_RE =
/(?:^|\/)(?:docker-compose[^/]*\.ya?ml|compose[^/]*\.ya?ml|values(?:\.[^/]+)?\.ya?ml|\.env(?:\.[^/]+)?|.*\.(?:tf|tfvars|hcl|ya?ml|json|toml|ini|conf|env)|Dockerfile(?:\.[^/]+)?|nginx[^/]*\.conf)$/i;

const HTTP_URL_RE = /https?:\/\/[^\s'"\`<>]+/gi;
const IP_ENDPOINT_RE = /\b(?:\d{1,3}\.){3}\d{1,3}:\d{1,5}\b/g;

const ALLOWLISTED_HOSTS = new Set(["localhost", "127.0.0.1", "example.com"]);

function isConfigPath(path: string): boolean {
return CONFIG_PATH_RE.test(path);
}

function isScannablePath(path: string): boolean {
return isMagicNumberSourcePath(path) && !isConfigPath(path);
}

function redactHost(host: string): string {
const lower = host.toLowerCase();
if (lower.length <= MAX_HOST_CHARS) return lower;
return `${lower.slice(0, MAX_HOST_CHARS - 3)}...`;
}

function isAllowlistedHost(host: string): boolean {
const lower = host.toLowerCase();
if (ALLOWLISTED_HOSTS.has(lower)) return true;
if (lower.endsWith(".example.com")) return true;
return false;
}

function hostFromHttpUrl(url: string): string {
const match = /^https?:\/\/([^/?#:]+)(?::\d+)?/i.exec(url);
return match?.[1] ?? url;
}

function isCommentLine(line: string): boolean {
const trimmed = line.trimStart();
return /^(?:\/\/|#|\/\*|\*|<!--)/.test(trimmed);
}

function isImportLine(line: string): boolean {
const trimmed = line.trimStart();
return /^(?:import\b|from\b|#include\b|require\s*\(|use\s+\w+::)/.test(trimmed);
}

/** Classify one added line for a hardcoded URL or IP endpoint, or null. Pure. */
export function detectHardcodedUrl(
line: string,
): { kind: HardcodedUrlFinding["kind"]; host: string } | null {
if (isCommentLine(line) || isImportLine(line)) return null;

HTTP_URL_RE.lastIndex = 0;
const urlMatch = HTTP_URL_RE.exec(line);
if (urlMatch) {
const host = hostFromHttpUrl(urlMatch[0]);
if (!isAllowlistedHost(host)) {
return { kind: "http-url", host: redactHost(host) };
}
}

IP_ENDPOINT_RE.lastIndex = 0;
const ipMatch = IP_ENDPOINT_RE.exec(line);
if (ipMatch) {
const host = ipMatch[0].split(":")[0] ?? ipMatch[0];
if (!isAllowlistedHost(host)) {
return { kind: "ip-endpoint", host: redactHost(host) };
}
}

return null;
}

type ScanLimits = {
maxFindings?: number;
signal?: AbortSignal;
};

/** Scan one file patch's added lines for hardcoded URLs/endpoints, line-cited via hunk headers. Pure. */
export function scanPatchForHardcodedUrl(
path: string,
patch: string,
limits: ScanLimits = {},
): HardcodedUrlFinding[] {
const maxFindings = limits.maxFindings ?? MAX_FINDINGS;
if (maxFindings <= 0 || !isScannablePath(path)) return [];
const findings: HardcodedUrlFinding[] = [];
let newLine = 0;
let inHunk = false;
for (const line of patch.split("\n")) {
if (limits.signal?.aborted) throw new Error("analyzer_aborted");
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
if (hunk) {
newLine = Number(hunk[1]);
inHunk = true;
continue;
}
if (!inHunk) continue;
if (line.startsWith("+")) {
const body = line.slice(1);
if (body.length <= MAX_LINE_CHARS) {
const hit = detectHardcodedUrl(body);
if (hit) {
findings.push({ file: path, line: newLine, kind: hit.kind, host: hit.host });
if (findings.length >= maxFindings) return findings;
}
}
newLine++;
} else if (!line.startsWith("-") && !line.startsWith("\\")) {
newLine++;
}
}
return findings;
}

/** Analyzer entrypoint: scan every changed scannable file's added lines for hardcoded endpoints. */
export async function scanHardcodedUrl(
req: EnrichRequest,
signal?: AbortSignal,
): Promise<HardcodedUrlFinding[]> {
const findings: HardcodedUrlFinding[] = [];
for (const file of req.files ?? []) {
if (signal?.aborted) throw new Error("analyzer_aborted");
if (!file.patch) continue;
for (const finding of scanPatchForHardcodedUrl(file.path, file.patch, {
maxFindings: MAX_FINDINGS - findings.length,
signal,
})) {
findings.push(finding);
if (findings.length >= MAX_FINDINGS) return findings;
}
}
return findings;
}
30 changes: 30 additions & 0 deletions review-enrichment/src/analyzers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { dependencyAnalyzer } from "./dependency/descriptor.js";
import { scanDocCommentDrift } from "./doc-comment-drift.js";
import { scanDuplication } from "./duplication-scan.js";
import { scanEol } from "./eol-check.js";
import { scanHardcodedUrl } from "./hardcoded-url.js";
import { scanHeavyDependencies } from "./heavy-dependency.js";
import { scanHistory } from "./history.js";
import { scanIacMisconfig } from "./iac-misconfig.js";
Expand Down Expand Up @@ -136,6 +137,35 @@ export const ANALYZER_DESCRIPTORS = [
run: (req, { signal, analysis, diagnostics }) =>
scanHeavyDependencies(req, fetch, { signal, analysis, diagnostics }),
}),
descriptor({
name: "hardcodedUrl",
title: "Hardcoded URLs and endpoints",
category: "config",
cost: "local",
defaultEnabled: true,
requires: ["files"],
limits: { maxFindings: 25, maxLineChars: 2000, maxHostChars: 40 },
docs: {
summary:
"Flags absolute HTTP(S) URLs and raw IP:port endpoints newly added in non-test, non-config source.",
looksAt: "Added lines in changed source files, excluding tests, config manifests, and comment/import lines.",
reports: "File, line, kind (http-url or ip-endpoint), and a redacted host — never full paths or queries.",
network: "Pure local analyzer. No external network call.",
notes:
"localhost, 127.0.0.1, and example.com are allowlisted. Distinct from the secret scanner — no credential detection.",
},
render: (findings, helpers) => {
if (!findings.length) return [];
const lines = ["### Hardcoded URLs/endpoints (should usually come from config)"];
for (const item of findings) {
lines.push(
`- ${helpers.safeCodeSpan(`${item.file}:${item.line}`)} — ${helpers.safeCodeSpan(item.kind)} host ${helpers.safeCodeSpan(item.host)}`,
);
}
return lines;
},
run: (req, { signal }) => scanHardcodedUrl(req, signal),
}),
descriptor({
name: "actionPin",
title: "Unpinned GitHub Actions",
Expand Down
1 change: 1 addition & 0 deletions review-enrichment/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ export function renderBrief(
lines.push(...renderDescriptorSection("magicNumber", findings.magicNumber));
lines.push(...renderDescriptorSection("conflictMarker", findings.conflictMarker));
lines.push(...renderDescriptorSection("debugLeftover", findings.debugLeftover));
lines.push(...renderDescriptorSection("hardcodedUrl", findings.hardcodedUrl));
lines.push(...renderDescriptorSection("commitLint", findings.commitLint));

if (!lines.length) return { promptSection: "", systemSuffix: "" };
Expand Down
10 changes: 10 additions & 0 deletions review-enrichment/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,15 @@ export interface DebugLeftoverFinding {
kind: "debugger" | "console" | "print";
}

/** An absolute HTTP(S) URL or raw IP:port endpoint hardcoded in non-test, non-config source (#2027, part of #1499).
* Reports location, kind, and a redacted/truncated host — never full paths or query strings. */
export interface HardcodedUrlFinding {
file: string;
line: number;
kind: "http-url" | "ip-endpoint";
host: string;
}

/** A PR commit subject that does not conform to the Conventional Commits spec (#2021, part of #1499). Reports a
* short SHA prefix, the subject, and the failing reason — never author/email. */
export interface CommitLintFinding {
Expand Down Expand Up @@ -519,6 +528,7 @@ export interface BriefFindings {
magicNumber?: MagicNumberFinding[];
conflictMarker?: ConflictMarkerFinding[];
debugLeftover?: DebugLeftoverFinding[];
hardcodedUrl?: HardcodedUrlFinding[];
commitLint?: CommitLintFinding[];
}

Expand Down
1 change: 1 addition & 0 deletions review-enrichment/test/analyzer-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const EXPECTED_ANALYZERS = [
"license",
"installScript",
"heavyDependency",
"hardcodedUrl",
"actionPin",
"eol",
"redos",
Expand Down
Loading
Loading