@codethreat/appsec-cli
Version:
CodeThreat AppSec CLI for CI/CD integration and automated security scanning
1,124 lines (1,109 loc) • 62.6 kB
JavaScript
#!/usr/bin/env node
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
CodeThreatApiClient: () => CodeThreatApiClient
});
module.exports = __toCommonJS(index_exports);
var import_commander6 = require("commander");
var import_chalk7 = __toESM(require("chalk"));
// src/commands/auth.ts
var import_commander = require("commander");
var import_chalk2 = __toESM(require("chalk"));
var import_inquirer = __toESM(require("inquirer"));
var import_path2 = __toESM(require("path"));
var import_os2 = __toESM(require("os"));
var import_fs_extra2 = __toESM(require("fs-extra"));
// src/lib/api-client.ts
var import_axios = __toESM(require("axios"));
var import_chalk = __toESM(require("chalk"));
// src/config/config.ts
var import_fs_extra = __toESM(require("fs-extra"));
var import_path = __toESM(require("path"));
var import_os = __toESM(require("os"));
var import_yaml = __toESM(require("yaml"));
var import_dotenv = __toESM(require("dotenv"));
function loadEnvFile() {
const envPaths = [
"./.env",
// Project-specific
import_path.default.join(import_os.default.homedir(), ".codethreat", ".env")
// User-specific
];
for (const envPath of envPaths) {
if (import_fs_extra.default.existsSync(envPath)) {
import_dotenv.default.config({ path: envPath });
break;
}
}
}
function loadSavedCredentials() {
try {
const credentialsPath = import_path.default.join(import_os.default.homedir(), ".codethreat", ".credentials");
if (import_fs_extra.default.existsSync(credentialsPath)) {
const credentials = JSON.parse(import_fs_extra.default.readFileSync(credentialsPath, "utf8"));
return {
apiKey: credentials.apiKey,
serverUrl: credentials.serverUrl
};
}
} catch (error) {
}
return {};
}
function getEnvConfig() {
loadEnvFile();
const savedCredentials = loadSavedCredentials();
return {
serverUrl: process.env.CT_SERVER_URL || savedCredentials.serverUrl || process.env.CT_PRODUCTION_URL || "https://app.codethreat.com",
apiKey: process.env.CT_API_KEY || savedCredentials.apiKey,
organizationId: process.env.CT_ORG_ID,
defaultScanTypes: (process.env.CT_DEFAULT_SCAN_TYPES || "sast,sca,secrets").split(","),
defaultBranch: process.env.CT_DEFAULT_BRANCH || "main",
defaultTimeout: parseInt(process.env.CT_TIMEOUT || "3600"),
defaultPollInterval: parseInt(process.env.CT_POLL_INTERVAL || "10"),
defaultFormat: process.env.CT_DEFAULT_FORMAT || "json",
outputDir: process.env.CT_OUTPUT_DIR || "./codethreat-results",
failOnHigh: process.env.CT_FAIL_ON_HIGH === "true",
failOnCritical: process.env.CT_FAIL_ON_CRITICAL !== "false",
// Default true
maxViolations: process.env.CT_MAX_VIOLATIONS ? parseInt(process.env.CT_MAX_VIOLATIONS) : void 0,
verbose: process.env.CT_VERBOSE === "true",
colors: process.env.CT_COLORS !== "false"
// Default true
};
}
var DEFAULT_CONFIG = getEnvConfig();
var currentConfig = { ...DEFAULT_CONFIG };
var CONFIG_LOCATIONS = [
"./.codethreat.yml",
// Project-specific
"./.codethreat.yaml",
// Project-specific (alternative)
import_path.default.join(import_os.default.homedir(), ".codethreat", "config.yml"),
// User-specific
import_path.default.join(import_os.default.homedir(), ".codethreat.yml")
// User-specific (legacy)
];
function loadConfig() {
currentConfig = { ...DEFAULT_CONFIG };
for (const configPath of CONFIG_LOCATIONS) {
if (import_fs_extra.default.existsSync(configPath)) {
try {
const configFile = import_fs_extra.default.readFileSync(configPath, "utf8");
const fileConfig = import_yaml.default.parse(configFile);
currentConfig = {
...currentConfig,
...fileConfig
};
console.log(`Loaded configuration from: ${configPath}`);
break;
} catch (error) {
console.warn(`Warning: Failed to load config from ${configPath}:`, error);
}
}
}
if (process.env.CT_API_KEY) currentConfig.apiKey = process.env.CT_API_KEY;
if (process.env.CT_SERVER_URL) currentConfig.serverUrl = process.env.CT_SERVER_URL;
if (process.env.CT_ORG_ID) currentConfig.organizationId = process.env.CT_ORG_ID;
if (process.env.CT_ORG_SLUG) currentConfig.organizationSlug = process.env.CT_ORG_SLUG;
if (process.env.CT_VERBOSE === "true") currentConfig.verbose = true;
validateConfig(currentConfig);
return currentConfig;
}
function getConfig() {
return currentConfig;
}
function updateConfig(updates) {
currentConfig = {
...currentConfig,
...updates
};
}
function saveConfig(config) {
const configDir = import_path.default.join(import_os.default.homedir(), ".codethreat");
const configPath = import_path.default.join(configDir, "config.yml");
import_fs_extra.default.ensureDirSync(configDir);
const newConfig = {
...currentConfig,
...config
};
const configToSave = { ...newConfig };
delete configToSave.apiKey;
const yamlContent = import_yaml.default.stringify(configToSave, {
indent: 2,
lineWidth: 120
});
import_fs_extra.default.writeFileSync(configPath, yamlContent, "utf8");
console.log(`Configuration saved to: ${configPath}`);
currentConfig = newConfig;
}
function validateConfig(config) {
if (!config.serverUrl) {
throw new Error("Server URL is required");
}
if (!config.serverUrl.startsWith("http")) {
throw new Error("Server URL must start with http:// or https://");
}
if (config.defaultTimeout < 60 || config.defaultTimeout > 3600) {
throw new Error("Default timeout must be between 60 and 3600 seconds");
}
if (config.defaultPollInterval < 5 || config.defaultPollInterval > 60) {
throw new Error("Default poll interval must be between 5 and 60 seconds");
}
}
function getConfigTemplate() {
return `# CodeThreat CLI Configuration
# This file can be placed in your project root or home directory
# Server configuration
server_url: "https://api.codethreat.com"
organization_id: "your-org-id" # Optional: Set default organization
# Default scan settings
default_scan_types: ["sast", "sca", "secrets"]
default_branch: "main"
default_timeout: 1800 # 30 minutes
default_poll_interval: 10 # 10 seconds
# Output settings
default_format: "json"
output_dir: "./codethreat-results"
# CI/CD behavior
fail_on_high: false
fail_on_critical: true
max_violations: 50 # Optional: Fail if more than N violations
# CLI behavior
verbose: false
colors: true
# Note: API key should be set via environment variable CT_API_KEY
# for security reasons, not in this config file
`;
}
// src/lib/api-client.ts
var CodeThreatApiClient = class {
client;
config = getConfig();
constructor() {
this.config = getConfig();
const serverUrl = process.env.CT_SERVER_URL || this.config.serverUrl;
const timeout = parseInt(process.env.CT_API_TIMEOUT || "30000");
const userAgent = `${process.env.CLI_NAME || "CodeThreat-CLI"}/${process.env.CLI_VERSION || "1.0.0"}`;
this.client = import_axios.default.create({
baseURL: serverUrl,
timeout,
headers: {
"Content-Type": "application/json",
"User-Agent": userAgent
}
});
this.client.interceptors.request.use((config) => {
const apiKey = this.config.apiKey || process.env.CT_API_KEY;
if (apiKey) {
config.headers["X-API-Key"] = apiKey;
}
return config;
});
this.client.interceptors.response.use(
(response) => {
if (this.config.verbose) {
console.log(import_chalk.default.gray(`\u2190 ${response.status} ${response.config.url}`));
}
return response;
},
(error) => {
this.handleApiError(error);
throw error;
}
);
}
/**
* Validate authentication
*/
async validateAuth(options = {}) {
const params = Object.entries(options).reduce((acc, [key, value]) => {
if (typeof value === "boolean") {
acc[key] = value.toString();
} else if (value !== void 0) {
acc[key] = value;
}
return acc;
}, {});
const response = await this.client.get(
"/api/v1/cli/auth/validate",
{ params }
);
return this.handleResponse(response);
}
/**
* Get CLI information and capabilities
*/
async getCLIInfo() {
const response = await this.client.get("/api/v1/cli/info");
return this.handleResponse(response);
}
/**
* Import repository from Git URL
*/
async importRepository(options) {
const organizationSlug = options.organizationSlug || this.config.organizationSlug || process.env.CT_ORG_SLUG || "";
const requestBody = {
url: options.url,
organizationSlug
};
if (options.name) requestBody.name = options.name;
if (options.provider) requestBody.provider = options.provider;
if (options.branch) requestBody.branch = options.branch;
if (options.autoScan !== void 0) requestBody.autoScan = options.autoScan;
if (options.scanTypes) requestBody.scanTypes = options.scanTypes;
if (options.isPrivate !== void 0) requestBody.isPrivate = options.isPrivate;
if (options.description) requestBody.description = options.description;
if (!requestBody.organizationSlug) {
throw new Error("Organization slug is required. Please set CT_ORG_SLUG environment variable or provide organizationSlug parameter.");
}
const response = await this.client.post(
"/api/v1/repositories/import",
requestBody
);
return this.handleResponse(response);
}
/**
* Get repository status
*/
async getRepositoryStatus(repositoryId) {
const response = await this.client.get(
`/api/v1/repositories/${repositoryId}/status`
);
return this.handleResponse(response);
}
/**
* Run scan (synchronous or asynchronous)
*/
async runScan(options) {
const organizationSlug = options.organizationSlug || this.config.organizationSlug || process.env.CT_ORG_SLUG || "";
const requestBody = {
repositoryId: options.repositoryId,
organizationSlug,
scanTypes: options.scanTypes
};
if (options.branch) requestBody.branch = options.branch;
if (options.wait !== void 0) requestBody.wait = options.wait;
if (options.timeout !== void 0) requestBody.timeout = options.timeout;
if (options.pollInterval !== void 0) requestBody.pollInterval = options.pollInterval;
if (options.scanTrigger) requestBody.scanTrigger = options.scanTrigger;
if (options.pullRequestId) requestBody.pullRequestId = options.pullRequestId;
if (options.commitSha) requestBody.commitSha = options.commitSha;
if (options.metadata) requestBody.metadata = options.metadata;
if (!requestBody.organizationSlug) {
throw new Error("Organization slug is required. Please set CT_ORG_SLUG environment variable or provide organizationSlug parameter.");
}
const response = await this.client.post(
"/api/v1/scans/run",
requestBody,
{
timeout: options.wait ? (options.timeout || 43200) * 1e3 + 3e4 : 3e4
// Default 12 hours for long scans, add 30s buffer for API processing
}
);
return this.handleResponse(response);
}
/**
* Get scan status
*/
async getScanStatus(scanId, includeLogs = false) {
const params = { includeLogs };
if (this.config.organizationSlug || process.env.CT_ORG_SLUG) {
params.organizationSlug = this.config.organizationSlug || process.env.CT_ORG_SLUG;
}
const response = await this.client.get(
`/api/v1/scans/${scanId}/status`,
{ params }
);
return this.handleResponse(response);
}
/**
* Export scan results
*/
async exportScanResults(options) {
const { scanId, ...params } = options;
const requestParams = { ...params };
if (this.config.organizationSlug || process.env.CT_ORG_SLUG) {
requestParams.organizationSlug = this.config.organizationSlug || process.env.CT_ORG_SLUG;
}
const response = await this.client.get(
`/api/v1/scans/${scanId}/results`,
{ params: requestParams }
);
return this.handleResponse(response);
}
/**
* Get organization configuration
*/
async getOrganizationConfig(organizationId) {
const response = await this.client.get(
`/api/v1/organizations/${organizationId}/config`
);
return this.handleResponse(response);
}
/**
* List repositories
*/
async listRepositories(options = {}) {
const response = await this.client.get(
"/api/v1/repositories",
{ params: options }
);
return this.handleResponse(response);
}
/**
* List scans
*/
async listScans(options = {}) {
const response = await this.client.get(
"/api/v1/scans",
{ params: options }
);
return this.handleResponse(response);
}
/**
* Handle API response and extract data
*/
handleResponse(response) {
const { data } = response;
if (!data.success) {
throw new Error(data.error?.message || "API request failed");
}
if (!data.data) {
throw new Error("No data in API response");
}
return data.data;
}
/**
* Handle API errors with user-friendly messages
*/
handleApiError(error) {
if (error.response) {
const status = error.response.status;
const data = error.response.data;
switch (status) {
case 401:
console.error(import_chalk.default.red("Authentication failed. Please check your API key."));
console.log(import_chalk.default.yellow("Run: codethreat auth login --api-key <your-key>"));
break;
case 403:
console.error(import_chalk.default.red("Permission denied. You may not have access to this resource."));
break;
case 404:
console.error(import_chalk.default.red("Resource not found. Please check the ID and try again."));
break;
case 429:
console.error(import_chalk.default.red("Rate limit exceeded. Please wait and try again."));
break;
case 500:
console.error(import_chalk.default.red("Server error. Please try again later."));
break;
default:
console.error(import_chalk.default.red(`API Error (${status}): ${data?.error?.message || error.message}`));
}
} else if (error.code === "ECONNREFUSED") {
console.error(import_chalk.default.red("Cannot connect to CodeThreat server. Please check your server URL."));
console.log(import_chalk.default.yellow(`Current server: ${this.config.serverUrl}`));
} else if (error.code === "ETIMEDOUT") {
console.error(import_chalk.default.red("Request timeout. The operation took too long."));
} else {
console.error(import_chalk.default.red("Network error:"), error.message);
}
}
/**
* Test API connectivity
*/
async testConnection() {
try {
await this.client.get("/api/v1/health");
return true;
} catch (error) {
return false;
}
}
/**
* Update API key
*/
setApiKey(apiKey) {
this.config.apiKey = apiKey;
this.client.defaults.headers["X-API-Key"] = apiKey;
}
/**
* Update server URL
*/
setServerUrl(serverUrl) {
this.config.serverUrl = serverUrl;
this.client.defaults.baseURL = serverUrl;
}
};
var apiClient = new CodeThreatApiClient();
// src/commands/auth.ts
var authCommand = new import_commander.Command("auth").description("Authentication management").addCommand(
new import_commander.Command("login").description("Login with API key").option("-k, --api-key <key>", "CodeThreat API key").option("-s, --server-url <url>", "CodeThreat server URL").option("--save", "Save credentials to config file", true).action(async (options) => {
try {
let { apiKey, serverUrl } = options;
const config = getConfig();
if (!apiKey) {
const answers = await import_inquirer.default.prompt([
{
type: "password",
name: "apiKey",
message: "Enter your CodeThreat API key:",
mask: "*",
validate: (input) => input.length > 0 || "API key is required"
}
]);
apiKey = answers.apiKey;
}
if (!serverUrl) {
const answers = await import_inquirer.default.prompt([
{
type: "input",
name: "serverUrl",
message: "Enter CodeThreat server URL:",
default: config.serverUrl,
validate: (input) => {
if (!input.startsWith("http")) {
return "Server URL must start with http:// or https://";
}
return true;
}
}
]);
serverUrl = answers.serverUrl;
}
if (serverUrl) apiClient.setServerUrl(serverUrl);
apiClient.setApiKey(apiKey);
console.log(import_chalk2.default.blue("\u{1F510} Validating authentication..."));
const authResult = await apiClient.validateAuth({
includeOrganizations: true,
includePermissions: true
});
if (!authResult.valid) {
throw new Error("Invalid API key");
}
console.log(import_chalk2.default.green("\u2705 Authentication successful!"));
console.log();
console.log(import_chalk2.default.bold("User Information:"));
console.log(` Name: ${authResult.user.name || "Not set"}`);
console.log(` Email: ${authResult.user.email}`);
console.log();
if (authResult.organizations && authResult.organizations.length > 0) {
console.log(import_chalk2.default.bold("Organizations:"));
authResult.organizations.forEach((org, index) => {
const marker = org.isPersonal ? "\u{1F464}" : "\u{1F3E2}";
console.log(` ${marker} ${org.name} (${org.planType})`);
if (index === 0) {
console.log(import_chalk2.default.gray(" ^ Default organization"));
}
});
console.log();
}
if (options.save) {
const configDir = import_path2.default.join(import_os2.default.homedir(), ".codethreat");
const credentialsPath = import_path2.default.join(configDir, ".credentials");
import_fs_extra2.default.ensureDirSync(configDir);
const credentials = {
apiKey,
serverUrl: serverUrl || config.serverUrl,
savedAt: (/* @__PURE__ */ new Date()).toISOString()
};
import_fs_extra2.default.writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2), {
mode: 384
// Only readable by user
});
saveConfig({
serverUrl: serverUrl || config.serverUrl,
organizationId: authResult.organizations?.[0]?.id
});
console.log(import_chalk2.default.green("\u{1F4BE} Credentials saved securely"));
console.log(import_chalk2.default.yellow("\u{1F4A1} Authentication will persist across CLI sessions"));
}
console.log(import_chalk2.default.green("\u{1F680} Ready to use CodeThreat CLI!"));
console.log(import_chalk2.default.gray("Try: codethreat repo list"));
} catch (error) {
console.error(import_chalk2.default.red("\u274C Login failed:"), error instanceof Error ? error.message : "Unknown error");
process.exit(1);
}
})
).addCommand(
new import_commander.Command("validate").description("Validate current authentication").option("--verbose", "Show detailed information").action(async (options) => {
try {
console.log(import_chalk2.default.blue("\u{1F510} Validating authentication..."));
const authResult = await apiClient.validateAuth({
includeOrganizations: true,
includePermissions: options.verbose,
includeUsage: options.verbose
});
console.log(import_chalk2.default.green("\u2705 Authentication valid"));
console.log();
console.log(import_chalk2.default.bold("User:"));
console.log(` ${authResult.user.name || "Not set"} (${authResult.user.email})`);
console.log();
if (authResult.organizations && authResult.organizations.length > 0) {
console.log(import_chalk2.default.bold("Organizations:"));
authResult.organizations.forEach((org) => {
const marker = org.isPersonal ? "\u{1F464}" : "\u{1F3E2}";
console.log(` ${marker} ${org.name} (${org.planType})`);
if (org.usageBalance !== void 0) {
console.log(import_chalk2.default.gray(` Balance: $${org.usageBalance.toFixed(2)}`));
}
});
console.log();
}
if (options.verbose && authResult.permissions) {
console.log(import_chalk2.default.bold("Permissions:"));
authResult.permissions.forEach((permission) => {
console.log(` \u2022 ${permission}`);
});
console.log();
}
console.log(import_chalk2.default.gray(`Authenticated at: ${authResult.authenticatedAt}`));
} catch (error) {
console.error(import_chalk2.default.red("\u274C Authentication invalid:"), error instanceof Error ? error.message : "Unknown error");
console.log(import_chalk2.default.yellow("Run: codethreat auth login"));
process.exit(1);
}
})
).addCommand(
new import_commander.Command("logout").description("Clear stored credentials").action(async () => {
try {
delete process.env.CT_API_KEY;
delete process.env.CT_SERVER_URL;
const credentialsPath = import_path2.default.join(import_os2.default.homedir(), ".codethreat", ".credentials");
if (import_fs_extra2.default.existsSync(credentialsPath)) {
import_fs_extra2.default.removeSync(credentialsPath);
}
saveConfig({
apiKey: void 0,
organizationId: void 0
});
console.log(import_chalk2.default.green("\u2705 Logged out successfully"));
console.log(import_chalk2.default.gray("Credentials cleared from all locations"));
console.log(import_chalk2.default.yellow("\u{1F4A1} You will need to login again to use the CLI"));
} catch (error) {
console.error(import_chalk2.default.red("\u274C Logout failed:"), error instanceof Error ? error.message : "Unknown error");
process.exit(1);
}
})
).addCommand(
new import_commander.Command("status").description("Show current authentication status").action(async () => {
try {
const config = getConfig();
console.log(import_chalk2.default.bold("Authentication Status:"));
console.log(` Server URL: ${config.serverUrl}`);
console.log(` API Key: ${config.apiKey ? import_chalk2.default.green("Set") : import_chalk2.default.red("Not set")}`);
console.log(` Organization: ${config.organizationId || import_chalk2.default.gray("Not set")}`);
console.log();
if (config.apiKey) {
console.log(import_chalk2.default.blue("Testing connection..."));
const isConnected = await apiClient.testConnection();
console.log(` Connection: ${isConnected ? import_chalk2.default.green("OK") : import_chalk2.default.red("Failed")}`);
}
} catch (error) {
console.error(import_chalk2.default.red("\u274C Status check failed:"), error instanceof Error ? error.message : "Unknown error");
}
})
);
// src/commands/repo.ts
var import_commander2 = require("commander");
var import_chalk3 = __toESM(require("chalk"));
var import_ora = __toESM(require("ora"));
var import_table = require("table");
var repoCommand = new import_commander2.Command("repo").description("Repository management").addCommand(
new import_commander2.Command("import").description("Import a repository from Git URL").argument("<url>", "Git repository URL").requiredOption("-org, --organization <slug>", "Organization slug (required)").option("-n, --name <name>", "Repository name (auto-detected from URL if not provided)").option("-p, --provider <provider>", "Git provider (github|gitlab|bitbucket|azure_devops)").option("-b, --branch <branch>", "Default branch", "main").option("--auto-scan", "Trigger scan after import", true).option("--no-auto-scan", "Skip auto-scan after import").option("-t, --types <types>", "Scan types (comma-separated)", "sast,sca,secrets").option("--private", "Mark repository as private").option("-d, --description <desc>", "Repository description").option("-f, --format <format>", "Output format (json|table)", "table").action(async (url, options) => {
try {
const config = getConfig();
const spinner = (0, import_ora.default)("Importing repository...").start();
const scanTypes = options.types.split(",").map((t) => t.trim());
const validScanTypes = ["sast", "sca", "secrets", "iac"];
const invalidTypes = scanTypes.filter((t) => !validScanTypes.includes(t));
if (invalidTypes.length > 0) {
spinner.fail(`Invalid scan types: ${invalidTypes.join(", ")}`);
console.log(import_chalk3.default.yellow(`Valid types: ${validScanTypes.join(", ")}`));
process.exit(1);
}
const result = await apiClient.importRepository({
url,
organizationSlug: options.organization,
name: options.name,
provider: options.provider,
branch: options.branch,
autoScan: options.autoScan,
scanTypes,
isPrivate: options.private,
description: options.description
});
if (result.alreadyExists) {
spinner.info("Repository already imported");
} else {
spinner.succeed("Repository imported successfully");
}
if (options.format === "json") {
console.log(JSON.stringify(result, null, 2));
} else {
console.log();
console.log(import_chalk3.default.bold("Repository Details:"));
console.log(` ID: ${result.repository.id}`);
console.log(` Name: ${result.repository.name}`);
console.log(` Full Name: ${result.repository.fullName}`);
console.log(` URL: ${result.repository.url}`);
console.log(` Provider: ${result.repository.provider}`);
console.log(` Default Branch: ${result.repository.defaultBranch}`);
console.log(` Private: ${result.repository.isPrivate ? "Yes" : "No"}`);
if (result.scan) {
console.log();
console.log(import_chalk3.default.bold("Auto-Scan Triggered:"));
console.log(` Scan ID: ${result.scan.id}`);
console.log(` Status: ${result.scan.status}`);
console.log(` Types: ${result.scan.types.join(", ")}`);
console.log(` Branch: ${result.scan.branch}`);
console.log();
console.log(import_chalk3.default.yellow('\u{1F4A1} Use "codethreat scan status ' + result.scan.id + '" to check progress'));
}
}
} catch (error) {
(0, import_ora.default)().fail("Import failed");
console.error(import_chalk3.default.red("Error:"), error instanceof Error ? error.message : "Unknown error");
process.exit(1);
}
})
).addCommand(
new import_commander2.Command("list").description("List imported repositories").option("-p, --provider <provider>", "Filter by provider").option("-s, --search <term>", "Search repositories by name").option("--status <status>", "Filter by status").option("--page <page>", "Page number", "1").option("--limit <limit>", "Results per page", "20").option("-f, --format <format>", "Output format (json|table)", "table").action(async (options) => {
try {
const spinner = (0, import_ora.default)("Fetching repositories...").start();
const result = await apiClient.listRepositories({
provider: options.provider,
search: options.search,
status: options.status,
page: parseInt(options.page),
limit: parseInt(options.limit)
});
spinner.succeed(`Found ${result.repositories.length} repositories`);
if (options.format === "json") {
console.log(JSON.stringify(result, null, 2));
return;
}
if (result.repositories.length === 0) {
console.log(import_chalk3.default.yellow("No repositories found"));
console.log(import_chalk3.default.gray('Use "codethreat repo import <url>" to import a repository'));
return;
}
const tableData = [
["ID", "Name", "Provider", "Branch", "Private", "Last Scan"],
...result.repositories.map((repo) => [
repo.id.substring(0, 8) + "...",
repo.name,
repo.connection?.provider?.name || "Unknown",
repo.defaultBranch,
repo.isPrivate ? "\u{1F512}" : "\u{1F310}",
repo.lastScanAt ? new Date(repo.lastScanAt).toLocaleDateString() : "Never"
])
];
console.log();
console.log((0, import_table.table)(tableData, {
header: {
alignment: "center",
content: import_chalk3.default.bold("Repositories")
}
}));
if (result.pagination?.hasMore) {
console.log();
console.log(import_chalk3.default.gray(`Showing ${result.repositories.length} of ${result.pagination.total} repositories`));
console.log(import_chalk3.default.gray(`Use --page ${parseInt(options.page) + 1} to see more`));
}
} catch (error) {
(0, import_ora.default)().fail("Failed to fetch repositories");
console.error(import_chalk3.default.red("Error:"), error instanceof Error ? error.message : "Unknown error");
process.exit(1);
}
})
).addCommand(
new import_commander2.Command("status").description("Get repository status").argument("<repository-id>", "Repository ID").option("-f, --format <format>", "Output format (json|table)", "table").action(async (repositoryId, options) => {
try {
const spinner = (0, import_ora.default)("Fetching repository status...").start();
const status = await apiClient.getRepositoryStatus(repositoryId);
spinner.succeed("Repository status retrieved");
if (options.format === "json") {
console.log(JSON.stringify(status, null, 2));
return;
}
console.log();
console.log(import_chalk3.default.bold("Repository Information:"));
console.log(` ID: ${status.repository.id}`);
console.log(` Name: ${status.repository.name}`);
console.log(` Full Name: ${status.repository.fullName}`);
console.log(` URL: ${status.repository.url}`);
console.log(` Provider: ${status.repository.provider}`);
console.log(` Default Branch: ${status.repository.defaultBranch}`);
console.log(` Private: ${status.repository.isPrivate ? "Yes" : "No"}`);
console.log(` Imported: ${new Date(status.repository.importedAt).toLocaleString()}`);
console.log();
console.log(import_chalk3.default.bold("Scanning Information:"));
console.log(` Has Scans: ${status.scanning.hasScans ? "Yes" : "No"}`);
if (status.scanning.latestScan) {
const scan = status.scanning.latestScan;
console.log(` Latest Scan:`);
console.log(` ID: ${scan.id}`);
console.log(` Status: ${(void 0).formatScanStatus(scan.status)}`);
console.log(` Branch: ${scan.branch}`);
console.log(` Started: ${new Date(scan.startedAt).toLocaleString()}`);
if (scan.completedAt) {
console.log(` Completed: ${new Date(scan.completedAt).toLocaleString()}`);
}
console.log(` Types: ${scan.types.join(", ")}`);
console.log(` Violations: ${scan.violationCount}`);
if (scan.securityScore) {
console.log(` Security Score: ${scan.securityScore}/100`);
}
}
if (status.scanning.pendingJobs.length > 0) {
console.log();
console.log(import_chalk3.default.bold("Pending Jobs:"));
status.scanning.pendingJobs.forEach((job) => {
console.log(` \u2022 ${job.type} (${job.status})`);
});
}
console.log();
console.log(import_chalk3.default.bold("Capabilities:"));
console.log(` Can Scan: ${status.capabilities.canScan ? "\u2705" : "\u274C"}`);
console.log(` Supported Types: ${status.capabilities.supportedScanTypes.join(", ")}`);
} catch (error) {
(0, import_ora.default)().fail("Failed to get repository status");
console.error(import_chalk3.default.red("Error:"), error instanceof Error ? error.message : "Unknown error");
process.exit(1);
}
})
);
// src/commands/scan.ts
var import_commander3 = require("commander");
var import_chalk4 = __toESM(require("chalk"));
var import_ora2 = __toESM(require("ora"));
var import_fs_extra3 = __toESM(require("fs-extra"));
var import_table2 = require("table");
var scanCommand = new import_commander3.Command("scan").description("Security scanning operations").addCommand(
new import_commander3.Command("run").description("Run security scan on repository").argument("<repository-id>", "Repository ID to scan").requiredOption("-org, --organization <slug>", "Organization slug (required)").option("-b, --branch <branch>", "Branch to scan", "main").option("-t, --types <types>", "Scan types (comma-separated)", "sast,sca,secrets").option("-w, --wait", "Wait for scan completion", false).option("--timeout <seconds>", "Timeout in seconds", "3600").option("--poll-interval <seconds>", "Polling interval in seconds", "10").option("--trigger <trigger>", "Scan trigger type", "api").option("--pr <pr-id>", "Pull request ID (if scanning PR)").option("--commit <sha>", "Commit SHA").option("-f, --format <format>", "Output format (json|table)", "table").option("-o, --output <file>", "Output file (optional)").action(async (repositoryId, options) => {
try {
const config = getConfig();
const scanTypes = options.types.split(",").map((t) => t.trim());
const validScanTypes = ["sast", "sca", "secrets", "iac", "scan"];
const invalidTypes = scanTypes.filter((t) => !validScanTypes.includes(t));
if (invalidTypes.length > 0) {
console.error(import_chalk4.default.red(`Invalid scan types: ${invalidTypes.join(", ")}`));
console.log(import_chalk4.default.yellow(`Valid types: ${validScanTypes.join(", ")}`));
process.exit(1);
}
const spinner = (0, import_ora2.default)("Starting security scan...").start();
const result = await apiClient.runScan({
repositoryId,
organizationSlug: options.organization,
branch: options.branch,
scanTypes: [],
// Empty array for shift-ql compatibility
wait: options.wait,
timeout: parseInt(options.timeout),
pollInterval: parseInt(options.pollInterval),
scanTrigger: options.trigger,
pullRequestId: options.pr,
commitSha: options.commit
});
if (result.synchronous) {
spinner.succeed(`Scan completed in ${result.duration}s`);
} else {
spinner.succeed("Scan started");
}
if (options.format === "json") {
const output = JSON.stringify(result, null, 2);
if (options.output) {
await import_fs_extra3.default.writeFile(options.output, output);
console.log(import_chalk4.default.green(`Results saved to: ${options.output}`));
} else {
console.log(output);
}
} else {
console.log();
console.log(import_chalk4.default.bold("Scan Information:"));
console.log(` Scan ID: ${result.scan.id}`);
console.log(` Repository: ${result.scan.repositoryId}`);
console.log(` Branch: ${result.scan.branch}`);
console.log(` Status: ${formatScanStatus(result.scan.status)}`);
console.log(` Types: ${result.scan.types.join(", ")}`);
console.log(` Started: ${new Date(result.scan.startedAt).toLocaleString()}`);
if (result.synchronous && result.results) {
console.log();
console.log(import_chalk4.default.bold("Scan Results:"));
console.log(` Total Violations: ${result.results.total}`);
console.log(` Critical: ${import_chalk4.default.red(result.results.critical)}`);
console.log(` High: ${import_chalk4.default.yellow(result.results.high)}`);
console.log(` Medium: ${import_chalk4.default.blue(result.results.medium)}`);
console.log(` Low: ${import_chalk4.default.gray(result.results.low)}`);
if (result.scan.securityScore) {
console.log(` Security Score: ${result.scan.securityScore}/100`);
}
if (config.failOnCritical && result.results.critical > 0) {
console.log();
console.error(import_chalk4.default.red("\u274C Build should fail: Critical vulnerabilities found"));
process.exit(1);
}
if (config.failOnHigh && result.results.high > 0) {
console.log();
console.error(import_chalk4.default.red("\u274C Build should fail: High severity vulnerabilities found"));
process.exit(1);
}
if (config.maxViolations && result.results.total > config.maxViolations) {
console.log();
console.error(import_chalk4.default.red(`\u274C Build should fail: Too many violations (${result.results.total} > ${config.maxViolations})`));
process.exit(1);
}
} else {
console.log();
console.log(import_chalk4.default.yellow('\u{1F4A1} Use "codethreat scan status ' + result.scan.id + '" to check progress'));
}
}
} catch (error) {
(0, import_ora2.default)().fail("Scan failed");
console.error(import_chalk4.default.red("Error:"), error instanceof Error ? error.message : "Unknown error");
process.exit(1);
}
})
).addCommand(
new import_commander3.Command("status").description("Get scan status").argument("<scan-id>", "Scan ID").option("--logs", "Include detailed logs").option("-f, --format <format>", "Output format (json|table)", "table").action(async (scanId, options) => {
try {
const spinner = (0, import_ora2.default)("Fetching scan status...").start();
const status = await apiClient.getScanStatus(scanId, options.logs);
spinner.succeed("Scan status retrieved");
if (options.format === "json") {
console.log(JSON.stringify(status, null, 2));
return;
}
console.log();
console.log(import_chalk4.default.bold("Scan Information:"));
console.log(` ID: ${status.scan.id}`);
console.log(` Repository: ${status.scan.repository.name}`);
console.log(` Branch: ${status.scan.branch}`);
console.log(` Status: ${formatScanStatus(status.scan.status)}`);
console.log(` Types: ${status.scan.types.join(", ")}`);
console.log(` Started: ${new Date(status.scan.startedAt).toLocaleString()}`);
if (status.scan.completedAt) {
console.log(` Completed: ${new Date(status.scan.completedAt).toLocaleString()}`);
console.log(` Duration: ${status.scan.scanDuration}s`);
}
console.log();
console.log(import_chalk4.default.bold("Progress:"));
console.log(` Percentage: ${status.progress.percentage}%`);
console.log(` Phase: ${status.progress.currentPhase}`);
if (status.progress.estimatedCompletion) {
console.log(` ETA: ${status.progress.estimatedCompletion}`);
}
if (status.results.violationCount > 0) {
console.log();
console.log(import_chalk4.default.bold("Results:"));
console.log(` Total Violations: ${status.results.violationCount}`);
console.log(` Critical: ${import_chalk4.default.red(status.results.summary.critical)}`);
console.log(` High: ${import_chalk4.default.yellow(status.results.summary.high)}`);
console.log(` Medium: ${import_chalk4.default.blue(status.results.summary.medium)}`);
console.log(` Low: ${import_chalk4.default.gray(status.results.summary.low)}`);
if (Object.keys(status.results.byType).length > 0) {
console.log();
console.log(import_chalk4.default.bold("By Type:"));
Object.entries(status.results.byType).forEach(([type, count]) => {
console.log(` ${type}: ${count}`);
});
}
}
if (options.logs && status.logs) {
console.log();
console.log(import_chalk4.default.bold("Execution Logs:"));
status.logs.forEach((log) => {
const statusIcon = log.status === "COMPLETED" ? "\u2705" : log.status === "FAILED" ? "\u274C" : log.status === "RUNNING" ? "\u{1F504}" : "\u23F3";
console.log(` ${statusIcon} ${log.step} (${log.status})`);
if (log.error) {
console.log(import_chalk4.default.red(` Error: ${log.error}`));
}
});
}
if (status.scan.status === "COMPLETED") {
console.log();
console.log(import_chalk4.default.green("\u2705 Scan completed successfully"));
console.log(import_chalk4.default.yellow('\u{1F4A1} Use "codethreat scan results ' + scanId + '" to export results'));
} else if (status.scan.status === "FAILED") {
console.log();
console.log(import_chalk4.default.red("\u274C Scan failed"));
} else {
console.log();
console.log(import_chalk4.default.blue("\u{1F504} Scan in progress..."));
console.log(import_chalk4.default.gray("Use this command again to check progress"));
}
} catch (error) {
(0, import_ora2.default)().fail("Failed to get scan status");
console.error(import_chalk4.default.red("Error:"), error instanceof Error ? error.message : "Unknown error");
process.exit(1);
}
})
).addCommand(
new import_commander3.Command("results").description("Export scan results").argument("<scan-id>", "Scan ID").option("-f, --format <format>", "Export format (json|sarif|csv|xml|junit)", "json").option("-o, --output <file>", "Output file").option("--severity <levels>", "Filter by severity (comma-separated)", "critical,high,medium,low").option("--types <types>", "Filter by scan types (comma-separated)").option("--include-fixed", "Include fixed violations", false).option("--include-suppressed", "Include suppressed violations", false).option("--no-metadata", "Exclude metadata from results").action(async (scanId, options) => {
try {
const spinner = (0, import_ora2.default)("Exporting scan results...").start();
const severityLevels = options.severity.split(",").map((s) => s.trim());
const scanTypes = options.types ? options.types.split(",").map((t) => t.trim()) : void 0;
const result = await apiClient.exportScanResults({
scanId,
format: options.format,
severity: severityLevels,
scanTypes,
includeFixed: options.includeFixed,
includeSuppressed: options.includeSuppressed,
includeMetadata: !options.noMetadata
});
spinner.succeed("Results exported successfully");
let outputFile = options.output;
if (!outputFile) {
const extension = options.format === "sarif" ? "sarif" : options.format === "junit" ? "xml" : options.format;
outputFile = `codethreat-results.${extension}`;
}
const outputContent = typeof result.results === "string" ? result.results : JSON.stringify(result.results, null, 2);
await import_fs_extra3.default.writeFile(outputFile, outputContent);
console.log();
console.log(import_chalk4.default.green("\u2705 Results exported:"));
console.log(` File: ${outputFile}`);
console.log(` Format: ${result.format}`);
console.log(` Violations: ${result.summary.total}`);
console.log(` Critical: ${import_chalk4.default.red(result.summary.critical)}`);
console.log(` High: ${import_chalk4.default.yellow(result.summary.high)}`);
console.log(` Medium: ${import_chalk4.default.blue(result.summary.medium)}`);
console.log(` Low: ${import_chalk4.default.gray(result.summary.low)}`);
if (options.format === "sarif") {
console.log();
console.log(import_chalk4.default.blue("\u{1F4A1} GitHub Actions integration:"));
console.log(import_chalk4.default.gray(" - uses: github/codeql-action/upload-sarif@v3"));
console.log(import_chalk4.default.gray(" with:"));
console.log(import_chalk4.default.gray(` sarif_file: ${outputFile}`));
} else if (options.format === "junit") {
console.log();
console.log(import_chalk4.default.blue("\u{1F4A1} GitLab CI/CD integration:"));
console.log(import_chalk4.default.gray(" artifacts:"));
console.log(import_chalk4.default.gray(" reports:"));
console.log(import_chalk4.default.gray(` junit: ${outputFile}`));
}
} catch (error) {
(0, import_ora2.default)().fail("Export failed");
console.error(import_chalk4.default.red("Error:"), error instanceof Error ? error.message : "Unknown error");
process.exit(1);
}
})
).addCommand(
new import_commander3.Command("list").description("List scans").option("-r, --repository <id>", "Filter by repository ID").option("-s, --status <status>", "Filter by status").option("--page <page>", "Page number", "1").option("--limit <limit>", "Results per page", "20").option("-f, --format <format>", "Output format (json|table)", "table").action(async (options) => {
try {
const spinner = (0, import_ora2.default)("Fetching scans...").start();
const result = await apiClient.listScans({
repositoryId: options.repository,
status: options.status,
page: parseInt(options.page),
limit: parseInt(options.limit)
});
spinner.succeed(`Found ${result.scans.length} scans`);
if (options.format === "json") {
console.log(JSON.stringify(result, null, 2));
return;
}
if (result.scans.length === 0) {
console.log(import_chalk4.default.yellow("No scans found"));
console.log(import_chalk4.default.gray('Use "codethreat scan run <repository-id>" to start a scan'));
return;
}
const tableData = [
["ID", "Repository", "Branch", "Status", "Types", "Started", "Score"],
...result.scans.map((scan) => [
scan.id.substring(0, 8) + "...",
scan.repository?.name || scan.repositoryId.substring(0, 8) + "...",
scan.branch,
formatScanStatus(scan.status),
scan.types.join(","),
new Date(scan.startedAt).toLocaleDateString(),
scan.securityScore ? `${scan.securityScore}/100` : "N/A"
])
];
console.log();
console.log((0, import_table2.table)(tableData, {
header: {
alignment: "center",
content: import_chalk4.default.bold("Scans")
}
}));
if (result.pagination?.hasMore) {
console.log();
console.log(import_chalk4.default.gray(`Showing ${result.scans.length} of ${result.pagination.total} scans`));
console.log(import_chalk4.default.gray(`Use --page ${parseInt(options.page) + 1} to see more`));
}
} catch (error) {
(0, import_ora2.default)().fail("Failed to fetch scans");
console.error(import_chalk4.default.red("Error:"), error instanceof Error ? error.message : "Unknown error");
process.exit(1);
}
})
);
function formatScanStatus(status) {
switch (status) {
case "COMPLETED":
return import_chalk4.default.green(status);
case "SCANNING":
return import_chalk4.default.blue(status);
case "PENDING":
return import_chalk4.default.yellow(status);
case "FAILED":
return import_chalk4.default.red(status);
default:
return import_chalk4.default.gray(status);
}
}
// src/commands/config.ts
var import_commander4 = require("commander");
var import_chalk5 = __toESM(require("chalk"));
var import_inquirer2 = __toESM(require("inquirer"));
var import_table3 = require("table");
var import_fs_extra4 = __toESM(require("fs-extra"));
var configCommand = new import_commander4.Command("config").description("Configuration management").addCommand(
new import_commander4.Command("show").description("Show current configuration").option("-f, --format <format>", "Output format (json|table)", "table").action(async (options) => {
try {
const config = getConfig();
if (options.format === "json") {
const safeConfig = { ...config };
if (safeConfig.apiKey) {
safeConfig.apiKey = "***hidden***";
}
console.log(JSON.stringify(safeConfig, null, 2));
return;
}
const configData = [
["Setting", "Value"],
["Server URL", config.serverUrl],
["API Key", config.apiKey ? import_chalk5.default.green("Set") : import_chalk5.default.red("Not set")]