Skip to content

Commit c741060

Browse files
author
thyldrm
committed
feat: add organization slug support
1 parent d9505ce commit c741060

File tree

5 files changed

+79
-18
lines changed

5 files changed

+79
-18
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@codethreat/appsec-cli",
3-
"version": "1.1.0",
3+
"version": "1.12.1",
44
"description": "CodeThreat AppSec CLI for CI/CD integration and automated security scanning",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/commands/repo.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const repoCommand = new Command('repo')
1313
new Command('import')
1414
.description('Import a repository from Git URL')
1515
.argument('<url>', 'Git repository URL')
16+
.requiredOption('-org, --organization <slug>', 'Organization slug (required)')
1617
.option('-n, --name <name>', 'Repository name (auto-detected from URL if not provided)')
1718
.option('-p, --provider <provider>', 'Git provider (github|gitlab|bitbucket|azure_devops)')
1819
.option('-b, --branch <branch>', 'Default branch', 'main')
@@ -41,6 +42,7 @@ export const repoCommand = new Command('repo')
4142

4243
const result = await apiClient.importRepository({
4344
url,
45+
organizationSlug: options.organization,
4446
name: options.name,
4547
provider: options.provider as Provider,
4648
branch: options.branch,

src/commands/scan.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const scanCommand = new Command('scan')
1414
new Command('run')
1515
.description('Run security scan on repository')
1616
.argument('<repository-id>', 'Repository ID to scan')
17+
.requiredOption('-org, --organization <slug>', 'Organization slug (required)')
1718
.option('-b, --branch <branch>', 'Branch to scan', 'main')
1819
.option('-t, --types <types>', 'Scan types (comma-separated)', 'sast,sca,secrets')
1920
.option('-w, --wait', 'Wait for scan completion', false)
@@ -44,6 +45,7 @@ export const scanCommand = new Command('scan')
4445

4546
const result = await apiClient.runScan({
4647
repositoryId,
48+
organizationSlug: options.organization,
4749
branch: options.branch,
4850
scanTypes: [], // Empty array for shift-ql compatibility
4951
wait: options.wait,

src/config/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface CLIConfig {
1010
serverUrl: string;
1111
apiKey?: string;
1212
organizationId?: string;
13+
organizationSlug?: string;
1314

1415
// Default scan settings
1516
defaultScanTypes: ScanType[];
@@ -135,6 +136,7 @@ export function loadConfig(): CLIConfig {
135136
if (process.env.CT_API_KEY) currentConfig.apiKey = process.env.CT_API_KEY;
136137
if (process.env.CT_SERVER_URL) currentConfig.serverUrl = process.env.CT_SERVER_URL;
137138
if (process.env.CT_ORG_ID) currentConfig.organizationId = process.env.CT_ORG_ID;
139+
if (process.env.CT_ORG_SLUG) currentConfig.organizationSlug = process.env.CT_ORG_SLUG;
138140
if (process.env.CT_VERBOSE === 'true') currentConfig.verbose = true;
139141

140142
// Validate required configuration

src/lib/api-client.ts

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ export class CodeThreatApiClient {
2121
private config = getConfig();
2222

2323
constructor() {
24+
// Reload config to pick up environment variables set by Azure extension
25+
this.config = getConfig();
26+
2427
// Use environment variable or config for server URL
2528
const serverUrl = process.env.CT_SERVER_URL || this.config.serverUrl;
2629
const timeout = parseInt(process.env.CT_API_TIMEOUT || '30000');
@@ -42,10 +45,6 @@ export class CodeThreatApiClient {
4245
config.headers['X-API-Key'] = apiKey;
4346
}
4447

45-
if (this.config.verbose) {
46-
console.log(chalk.gray(`→ ${config.method?.toUpperCase()} ${config.url}`));
47-
}
48-
4948
return config;
5049
});
5150

@@ -103,6 +102,7 @@ export class CodeThreatApiClient {
103102
*/
104103
async importRepository(options: {
105104
url: string;
105+
organizationSlug?: string;
106106
name?: string;
107107
provider?: Provider;
108108
branch?: string;
@@ -111,9 +111,32 @@ export class CodeThreatApiClient {
111111
isPrivate?: boolean;
112112
description?: string;
113113
}): Promise<RepositoryImportResponse> {
114+
// Create request body - ensure organizationSlug is ALWAYS included
115+
const organizationSlug = options.organizationSlug || this.config.organizationSlug || process.env.CT_ORG_SLUG || '';
116+
117+
// Build clean request body without undefined values
118+
const requestBody: any = {
119+
url: options.url,
120+
organizationSlug: organizationSlug
121+
};
122+
123+
// Only add optional fields if they have values
124+
if (options.name) requestBody.name = options.name;
125+
if (options.provider) requestBody.provider = options.provider;
126+
if (options.branch) requestBody.branch = options.branch;
127+
if (options.autoScan !== undefined) requestBody.autoScan = options.autoScan;
128+
if (options.scanTypes) requestBody.scanTypes = options.scanTypes;
129+
if (options.isPrivate !== undefined) requestBody.isPrivate = options.isPrivate;
130+
if (options.description) requestBody.description = options.description;
131+
132+
// Validate organizationSlug is present
133+
if (!requestBody.organizationSlug) {
134+
throw new Error('Organization slug is required. Please set CT_ORG_SLUG environment variable or provide organizationSlug parameter.');
135+
}
136+
114137
const response = await this.client.post<ApiResponse<RepositoryImportResponse>>(
115138
'/api/v1/repositories/import',
116-
options
139+
requestBody
117140
);
118141

119142
return this.handleResponse(response);
@@ -135,6 +158,7 @@ export class CodeThreatApiClient {
135158
*/
136159
async runScan(options: {
137160
repositoryId: string;
161+
organizationSlug?: string;
138162
branch?: string;
139163
scanTypes: ScanType[];
140164
wait?: boolean;
@@ -145,11 +169,37 @@ export class CodeThreatApiClient {
145169
commitSha?: string;
146170
metadata?: Record<string, string>;
147171
}): Promise<ScanRunResponse> {
172+
// Get organizationSlug from options, config, or environment
173+
const organizationSlug = options.organizationSlug || this.config.organizationSlug || process.env.CT_ORG_SLUG || '';
174+
175+
// Build clean request body without undefined values
176+
const requestBody: any = {
177+
repositoryId: options.repositoryId,
178+
organizationSlug: organizationSlug,
179+
scanTypes: options.scanTypes
180+
};
181+
182+
// Only add optional fields if they have values
183+
if (options.branch) requestBody.branch = options.branch;
184+
if (options.wait !== undefined) requestBody.wait = options.wait;
185+
if (options.timeout !== undefined) requestBody.timeout = options.timeout;
186+
if (options.pollInterval !== undefined) requestBody.pollInterval = options.pollInterval;
187+
if (options.scanTrigger) requestBody.scanTrigger = options.scanTrigger;
188+
if (options.pullRequestId) requestBody.pullRequestId = options.pullRequestId;
189+
if (options.commitSha) requestBody.commitSha = options.commitSha;
190+
if (options.metadata) requestBody.metadata = options.metadata;
191+
192+
193+
// Validate organizationSlug is present
194+
if (!requestBody.organizationSlug) {
195+
throw new Error('Organization slug is required. Please set CT_ORG_SLUG environment variable or provide organizationSlug parameter.');
196+
}
197+
148198
const response = await this.client.post<ApiResponse<ScanRunResponse>>(
149199
'/api/v1/scans/run',
150-
options,
200+
requestBody,
151201
{
152-
timeout: options.wait ? (options.timeout || 1800) * 1000 + 30000 : 30000, // Add 30s buffer for API processing
202+
timeout: options.wait ? (options.timeout || 43200) * 1000 + 30000 : 30000, // Default 12 hours for long scans, add 30s buffer for API processing
153203
}
154204
);
155205

@@ -160,9 +210,15 @@ export class CodeThreatApiClient {
160210
* Get scan status
161211
*/
162212
async getScanStatus(scanId: string, includeLogs = false): Promise<ScanStatusResponse> {
213+
// Include organizationSlug if available
214+
const params: any = { includeLogs };
215+
if (this.config.organizationSlug || process.env.CT_ORG_SLUG) {
216+
params.organizationSlug = this.config.organizationSlug || process.env.CT_ORG_SLUG;
217+
}
218+
163219
const response = await this.client.get<ApiResponse<ScanStatusResponse>>(
164220
`/api/v1/scans/${scanId}/status`,
165-
{ params: { includeLogs } }
221+
{ params }
166222
);
167223

168224
return this.handleResponse(response);
@@ -182,9 +238,16 @@ export class CodeThreatApiClient {
182238
ruleIds?: string[];
183239
}): Promise<ScanResultsResponse> {
184240
const { scanId, ...params } = options;
241+
242+
// Include organizationSlug if available
243+
const requestParams: any = { ...params };
244+
if (this.config.organizationSlug || process.env.CT_ORG_SLUG) {
245+
requestParams.organizationSlug = this.config.organizationSlug || process.env.CT_ORG_SLUG;
246+
}
247+
185248
const response = await this.client.get<ApiResponse<ScanResultsResponse>>(
186249
`/api/v1/scans/${scanId}/results`,
187-
{ params }
250+
{ params: requestParams }
188251
);
189252

190253
return this.handleResponse(response);
@@ -246,10 +309,6 @@ export class CodeThreatApiClient {
246309
private handleResponse<T>(response: AxiosResponse<ApiResponse<T>>): T {
247310
const { data } = response;
248311

249-
if (this.config.verbose) {
250-
console.log(chalk.gray('Response data:'), JSON.stringify(data, null, 2));
251-
}
252-
253312
if (!data.success) {
254313
throw new Error(data.error?.message || 'API request failed');
255314
}
@@ -265,10 +324,6 @@ export class CodeThreatApiClient {
265324
* Handle API errors with user-friendly messages
266325
*/
267326
private handleApiError(error: AxiosError): void {
268-
if (this.config.verbose) {
269-
console.error(chalk.red('API Error Details:'), error.response?.data || error.message);
270-
}
271-
272327
if (error.response) {
273328
const status = error.response.status;
274329
const data = error.response.data as any;

0 commit comments

Comments
 (0)