UNPKG

@codethreat/appsec-cli

Version:

CodeThreat AppSec CLI for CI/CD integration and automated security scanning

1,124 lines (1,109 loc) 62.6 kB
#!/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")]