Skip to content
Draft
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
"test:coverage:ci": "vitest --coverage --reporter=verbose",
"test:run": "vitest run",
"coverage:check": "vitest --coverage --reporter=verbose --config vitest.config.coverage.ts",
"bump": "node scripts/bump.js"
"bump": "node scripts/bump.js",
"update:bugsnag-api": "tsx scripts/update-bugsnag-api.ts"
},
"dependencies": {
"@bugsnag/js": "^8.8.1",
Expand Down
205 changes: 205 additions & 0 deletions scripts/update-bugsnag-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import { execSync } from "child_process";
import * as fs from "fs";
import * as path from "path";
import { Node, Project, SyntaxKind } from "ts-morph";

const API_FILE_PATH = "src/bugsnag/client/api/api.ts";

const HEADER_COMMENT = `/** biome-ignore-all lint/complexity/useLiteralKeys: auto-generated code */
/** biome-ignore-all lint/correctness/noUnusedVariables: auto-generated code */
/** biome-ignore-all lint/complexity/useOptionalChain: auto-generated code */
/** biome-ignore-all lint/style/useLiteralEnumMembers: auto-generated code */
/** biome-ignore-all lint/suspicious/noEmptyInterface: auto-generated code */
// tslint:disable
/**
* BugSnag - Data Access API
*
* The following code is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
*
* To update:
* 1. Run \`npm run update:bugsnag\` to automatically download and prune unused code
* 2. Check \`git diff\` to ensure the changes are correct
*/

import * as url from "node:url";
import type { Configuration } from "./configuration";
`;

async function downloadAndExtractApi() {
console.log("Fetching Swagger spec from SmartBear...");
const specResponse = await fetch(
"https://api.swaggerhub.com/apis/smartbear-public/bugsnag-data-access-api/2/swagger.json",
);
if (!specResponse.ok) {
throw new Error(`Failed to fetch spec: ${specResponse.statusText}`);
}
const specJson = await specResponse.json();

console.log(
"Requesting TypeScript client generation from Swagger Generator...",
);
const genResponse = await fetch(
"https://generator.swagger.io/api/gen/clients/typescript-fetch",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
options: { modelPropertyNaming: "original" },
spec: specJson,
}),
},
);

if (!genResponse.ok) {
throw new Error(`Failed to generate client: ${genResponse.statusText}`);
}

const { link } = await genResponse.json();
if (!link) {
throw new Error("No download link returned from generator.");
}

console.log(`Downloading generated client from ${link}...`);
const zipPath = "/tmp/bugsnag-client.zip";
execSync(`curl -s "${link}" -o ${zipPath}`);

Check failure

Code scanning / CodeQL

Uncontrolled command line Critical

This command line depends on a
user-provided value
.

Copilot Autofix

AI 17 days ago

In general, the safest fix is to avoid constructing shell command strings from untrusted input and instead use child_process APIs that accept the command and its arguments as separate parameters without invoking a shell (execFileSync, spawnSync, etc.). This prevents shell metacharacters in the input from being interpreted as part of the command.

For this specific case, we should replace the execSync curl invocation with a call to execFileSync (from the same child_process module) and pass link and zipPath as separate arguments. That preserves existing behavior (still calling curl -s <link> -o <zipPath>) but no longer runs through a shell, so link is only ever treated as a single argument. Concretely, in scripts/update-bugsnag-api.ts around line 68, change:

  • execSync(\curl -s "${link}" -o ${zipPath}`);`

to:

  • execFileSync("curl", ["-s", link, "-o", zipPath]);

We also need to import execFileSync from child_process alongside the existing execSync import at the top of the same file. No other logic or files need to be changed.

Suggested changeset 1
scripts/update-bugsnag-api.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/scripts/update-bugsnag-api.ts b/scripts/update-bugsnag-api.ts
--- a/scripts/update-bugsnag-api.ts
+++ b/scripts/update-bugsnag-api.ts
@@ -1,4 +1,4 @@
-import { execSync } from "child_process";
+import { execSync, execFileSync } from "child_process";
 import * as fs from "fs";
 import * as path from "path";
 import { Node, Project, SyntaxKind } from "ts-morph";
@@ -65,7 +65,7 @@
 
   console.log(`Downloading generated client from ${link}...`);
   const zipPath = "/tmp/bugsnag-client.zip";
-  execSync(`curl -s "${link}" -o ${zipPath}`);
+  execFileSync("curl", ["-s", link, "-o", zipPath]);
 
   console.log("Extracting api.ts...");
   const extractedApi = execSync(
EOF
@@ -1,4 +1,4 @@
import { execSync } from "child_process";
import { execSync, execFileSync } from "child_process";
import * as fs from "fs";
import * as path from "path";
import { Node, Project, SyntaxKind } from "ts-morph";
@@ -65,7 +65,7 @@

console.log(`Downloading generated client from ${link}...`);
const zipPath = "/tmp/bugsnag-client.zip";
execSync(`curl -s "${link}" -o ${zipPath}`);
execFileSync("curl", ["-s", link, "-o", zipPath]);

console.log("Extracting api.ts...");
const extractedApi = execSync(
Copilot is powered by AI and may make mistakes. Always verify output.

console.log("Extracting api.ts...");
const extractedApi = execSync(
`unzip -p ${zipPath} typescript-fetch-client/api.ts`,
).toString("utf-8");

console.log("Preparing new api.ts content...");

// Find where the actual code starts (after the generated header and imports)
// Typically it starts with `export const BASE_PATH` or `export interface`
let rawCode = extractedApi.replace(
/\/\/\/ <reference path="\.\/custom\.d\.ts" \/>\n/,
"",
);
// Remove the generated header comment
rawCode = rawCode.replace(/\/\*\*[\s\S]*?\*\//, "").trim();
// Remove duplicate original imports, as we inject our own with node:url and type imports
rawCode = rawCode.replace(/import \* as url from "url";\n/, "");
rawCode = rawCode.replace(
/import { Configuration } from "\.\/configuration";\n/,
"",
);
rawCode = rawCode.replace(/\/\/ tslint:disable\n/, "");

fs.writeFileSync(API_FILE_PATH, HEADER_COMMENT + "\n" + rawCode.trimStart());
console.log(`Saved newly extracted base code to ${API_FILE_PATH}`);
}

async function main() {
await downloadAndExtractApi();

console.log(`Loading project and analyzing ${API_FILE_PATH}...`);
const project = new Project({
tsConfigFilePath: "tsconfig.json",
});

// Force a reload from disk since we just modified it
const sourceFile = project.getSourceFileOrThrow(API_FILE_PATH);
await sourceFile.refreshFromFileSystem();

console.log("Removing portable-fetch and isomorphic-fetch imports if any...");
for (const moduleName of ["isomorphic-fetch", "portable-fetch"]) {
const fetchImport = sourceFile.getImportDeclaration(
(decl) => decl.getModuleSpecifierValue() === moduleName,
);
if (fetchImport) {
fetchImport.remove();
}
}

console.log(
"Removing Fp and Factory constants, and object-oriented API classes...",
);

const varsToRemove = [];
for (const varDecl of sourceFile.getVariableDeclarations()) {
const name = varDecl.getName();
if (name.endsWith("ApiFp") || name.endsWith("ApiFactory")) {
varsToRemove.push(varDecl);
}
// Also remove unused Scim and Integrations creators
if (
name === "ScimApiFetchParamCreator" ||
name === "IntegrationsApiFetchParamCreator"
) {
varsToRemove.push(varDecl);
}
}

for (const v of varsToRemove) {
v.getVariableStatement()?.remove();
}

const classesToRemove = [];
for (const classDecl of sourceFile.getClasses()) {
const name = classDecl.getName() || "";
if (name.endsWith("Api")) {
classesToRemove.push(classDecl);
}
}
for (const c of classesToRemove) {
c.remove();
}

console.log(
"Removing BaseAPI, FetchAPI, BASE_PATH, and COLLECTION_FORMATS...",
);
sourceFile.getInterface("FetchAPI")?.remove();
sourceFile.getClass("BaseAPI")?.remove();
sourceFile.getVariableStatement("BASE_PATH")?.remove();
sourceFile.getVariableStatement("COLLECTION_FORMATS")?.remove();

console.log("Stripping unreferenced exports using ts-morph...");
const exportedDeclarations = sourceFile.getExportedDeclarations();
let strippedCount = 0;

for (const [name, declarations] of exportedDeclarations) {
for (const decl of declarations) {
if ("setIsExported" in decl && typeof decl.setIsExported === "function") {
const referencedNodes = decl.findReferencesAsNodes();
const isUsedOutside = referencedNodes.some(
(n) => n.getSourceFile().getFilePath() !== sourceFile.getFilePath(),
);

if (!isUsedOutside) {
(decl as any).setIsExported(false);
strippedCount++;
}
}
}
}

console.log(`Stripped ${strippedCount} unused exports.`);

console.log("Saving changes...");
await project.save();

console.log("Formatting the file with biome...");
try {
execSync(`npx biome check --write --unsafe ${API_FILE_PATH}`, {
stdio: "inherit",
});
} catch (err) {
console.error("Warning: biome format failed.", err);
}

console.log("Running pre-commit hooks on the generated file...");
try {
execSync(`prek run --files ${API_FILE_PATH}`, { stdio: "inherit" });
} catch (err) {
console.error("Warning: pre-commit hooks failed.", err);
}

console.log("BugSnag API update automation completed.");
}

main().catch(console.error);
Loading
Loading