| 
 | 1 | +#!/usr/bin/env ts-node-script  | 
 | 2 | + | 
 | 3 | +import { gh, GitHubDraftRelease, GitHubReleaseAsset } from "../src/github";  | 
 | 4 | +import { join } from "path";  | 
 | 5 | +import { exec, mkdir, mv, rm, zip } from "../src/shell";  | 
 | 6 | +import { prompt } from "enquirer";  | 
 | 7 | +import chalk from "chalk";  | 
 | 8 | +import * as fs from "node:fs";  | 
 | 9 | +import * as crypto from "crypto";  | 
 | 10 | +import { pipeline } from "stream/promises";  | 
 | 11 | +import { mkdtemp } from "node:fs/promises";  | 
 | 12 | +import { homedir, tmpdir } from "node:os";  | 
 | 13 | + | 
 | 14 | +// ============================================================================  | 
 | 15 | +// Constants  | 
 | 16 | +// ============================================================================  | 
 | 17 | + | 
 | 18 | +const SBOM_GENERATOR_JAR = join(homedir(), "SBOM_Generator.jar");  | 
 | 19 | + | 
 | 20 | +// ============================================================================  | 
 | 21 | +// Utility Functions  | 
 | 22 | +// ============================================================================  | 
 | 23 | + | 
 | 24 | +function printHeader(title: string): void {  | 
 | 25 | +    console.log("\n" + chalk.bold.cyan("═".repeat(60)));  | 
 | 26 | +    console.log(chalk.bold.cyan(`  ${title}`));  | 
 | 27 | +    console.log(chalk.bold.cyan("═".repeat(60)) + "\n");  | 
 | 28 | +}  | 
 | 29 | + | 
 | 30 | +function printStep(step: number, total: number, message: string): void {  | 
 | 31 | +    console.log(chalk.bold.blue(`\n[${step}/${total}]`) + chalk.white(` ${message}`));  | 
 | 32 | +}  | 
 | 33 | + | 
 | 34 | +function printSuccess(message: string): void {  | 
 | 35 | +    console.log(chalk.green(`✅ ${message}`));  | 
 | 36 | +}  | 
 | 37 | + | 
 | 38 | +function printError(message: string): void {  | 
 | 39 | +    console.log(chalk.red(`❌ ${message}`));  | 
 | 40 | +}  | 
 | 41 | + | 
 | 42 | +function printWarning(message: string): void {  | 
 | 43 | +    console.log(chalk.yellow(`⚠️  ${message}`));  | 
 | 44 | +}  | 
 | 45 | + | 
 | 46 | +function printInfo(message: string): void {  | 
 | 47 | +    console.log(chalk.cyan(`ℹ️  ${message}`));  | 
 | 48 | +}  | 
 | 49 | + | 
 | 50 | +function printProgress(message: string): void {  | 
 | 51 | +    console.log(chalk.gray(`   → ${message}`));  | 
 | 52 | +}  | 
 | 53 | + | 
 | 54 | +// ============================================================================  | 
 | 55 | +// Core Functions  | 
 | 56 | +// ============================================================================  | 
 | 57 | + | 
 | 58 | +async function verifyGitHubAuth(): Promise<void> {  | 
 | 59 | +    printStep(1, 6, "Verifying GitHub authentication...");  | 
 | 60 | + | 
 | 61 | +    try {  | 
 | 62 | +        await gh.ensureAuth();  | 
 | 63 | +        printSuccess("GitHub authentication verified");  | 
 | 64 | +    } catch (error) {  | 
 | 65 | +        printError(`GitHub authentication failed: ${(error as Error).message}`);  | 
 | 66 | +        console.log(chalk.yellow("\n💡 Setup Instructions:\n"));  | 
 | 67 | +        console.log(chalk.white("1. Install GitHub CLI:"));  | 
 | 68 | +        console.log(chalk.cyan("   • Download: https://cli.github.com/"));  | 
 | 69 | +        console.log(chalk.cyan("   • Or via brew: brew install gh\n"));  | 
 | 70 | +        console.log(chalk.white("2. Authenticate (choose one option):"));  | 
 | 71 | +        console.log(chalk.cyan("   • Option A: export GITHUB_TOKEN=your_token_here"));  | 
 | 72 | +        console.log(chalk.cyan("   • Option B: export GH_PAT=your_token_here"));  | 
 | 73 | +        console.log(chalk.cyan("   • Option C: gh auth login\n"));  | 
 | 74 | +        console.log(chalk.white("3. For A and B get your token at:"));  | 
 | 75 | +        console.log(chalk.cyan("   https://github.com/settings/tokens\n"));  | 
 | 76 | +        throw new Error("GitHub authentication required");  | 
 | 77 | +    }  | 
 | 78 | +}  | 
 | 79 | + | 
 | 80 | +async function selectRelease(): Promise<GitHubDraftRelease> {  | 
 | 81 | +    printStep(2, 6, "Fetching draft releases...");  | 
 | 82 | + | 
 | 83 | +    const releases = await gh.getDraftReleases();  | 
 | 84 | +    printSuccess(`Found ${releases.length} draft release${releases.length !== 1 ? "s" : ""}`);  | 
 | 85 | + | 
 | 86 | +    if (releases.length === 0) {  | 
 | 87 | +        printWarning("No draft releases found");  | 
 | 88 | +        throw new Error("No releases available");  | 
 | 89 | +    }  | 
 | 90 | + | 
 | 91 | +    console.log(); // spacing  | 
 | 92 | +    const { tag_name } = await prompt<{ tag_name: string }>({  | 
 | 93 | +        type: "select",  | 
 | 94 | +        name: "tag_name",  | 
 | 95 | +        message: "Select a release to process:",  | 
 | 96 | +        choices: releases.map(r => ({  | 
 | 97 | +            name: r.tag_name,  | 
 | 98 | +            message: `${r.name} ${chalk.gray(`(${r.tag_name})`)}`  | 
 | 99 | +        }))  | 
 | 100 | +    });  | 
 | 101 | + | 
 | 102 | +    const release = releases.find(r => r.tag_name === tag_name);  | 
 | 103 | +    if (!release) {  | 
 | 104 | +        throw new Error(`Release not found: ${tag_name}`);  | 
 | 105 | +    }  | 
 | 106 | + | 
 | 107 | +    printInfo(`Selected release: ${chalk.bold(release.name)}`);  | 
 | 108 | +    return release;  | 
 | 109 | +}  | 
 | 110 | + | 
 | 111 | +async function findAndValidateMpkAsset(release: GitHubDraftRelease): Promise<GitHubReleaseAsset> {  | 
 | 112 | +    printStep(3, 6, "Locating MPK asset...");  | 
 | 113 | + | 
 | 114 | +    const mpkAsset = release.assets.find(asset => asset.name.endsWith(".mpk"));  | 
 | 115 | + | 
 | 116 | +    if (!mpkAsset) {  | 
 | 117 | +        printError("No MPK asset found in release");  | 
 | 118 | +        printInfo(`Available assets: ${release.assets.map(a => a.name).join(", ")}`);  | 
 | 119 | +        throw new Error("MPK asset not found");  | 
 | 120 | +    }  | 
 | 121 | + | 
 | 122 | +    printSuccess(`Found MPK asset: ${chalk.bold(mpkAsset.name)}`);  | 
 | 123 | +    printInfo(`Asset ID: ${mpkAsset.id}`);  | 
 | 124 | +    return mpkAsset;  | 
 | 125 | +}  | 
 | 126 | + | 
 | 127 | +async function downloadAndVerifyAsset(mpkAsset: GitHubReleaseAsset, downloadPath: string): Promise<string> {  | 
 | 128 | +    printStep(4, 6, "Downloading and verifying MPK asset...");  | 
 | 129 | + | 
 | 130 | +    printProgress(`Downloading to: ${downloadPath}`);  | 
 | 131 | +    await gh.downloadReleaseAsset(mpkAsset.id, downloadPath);  | 
 | 132 | +    printSuccess("Download completed");  | 
 | 133 | + | 
 | 134 | +    printProgress("Computing SHA-256 hash...");  | 
 | 135 | +    const fileHash = await computeHash(downloadPath);  | 
 | 136 | +    printInfo(`Computed hash: ${fileHash}`);  | 
 | 137 | + | 
 | 138 | +    const expectedDigest = mpkAsset.digest.replace("sha256:", "");  | 
 | 139 | +    if (fileHash !== expectedDigest) {  | 
 | 140 | +        printError("Hash mismatch detected!");  | 
 | 141 | +        printInfo(`Expected: ${expectedDigest}`);  | 
 | 142 | +        printInfo(`Got:      ${fileHash}`);  | 
 | 143 | +        throw new Error("Asset integrity verification failed");  | 
 | 144 | +    }  | 
 | 145 | + | 
 | 146 | +    printSuccess("Hash verification passed");  | 
 | 147 | +    return fileHash;  | 
 | 148 | +}  | 
 | 149 | + | 
 | 150 | +async function runSbomGenerator(tmpFolder: string): Promise<void> {  | 
 | 151 | +    printStep(5, 6, "Running SBOM Generator...");  | 
 | 152 | + | 
 | 153 | +    printProgress("Unzipping MPK...");  | 
 | 154 | +    await exec(`java -jar ${SBOM_GENERATOR_JAR} SBOM_GENERATOR unzip`, { cwd: tmpFolder });  | 
 | 155 | +    printSuccess("MPK unzipped");  | 
 | 156 | + | 
 | 157 | +    printProgress("Scanning dependencies...");  | 
 | 158 | +    await exec(`java -jar ${SBOM_GENERATOR_JAR} SBOM_GENERATOR scan`, { cwd: tmpFolder });  | 
 | 159 | +    printSuccess("Scan completed");  | 
 | 160 | +}  | 
 | 161 | + | 
 | 162 | +async function createOutputArchive(tmpFolder: string, releaseName: string, fileHash: string): Promise<void> {  | 
 | 163 | +    printStep(6, 6, "Creating output archive...");  | 
 | 164 | + | 
 | 165 | +    const resultsFolder = join(tmpFolder, "CCA_JSON");  | 
 | 166 | +    const archiveName = `${releaseName}.zip`;  | 
 | 167 | +    const finalName = `${releaseName} [${fileHash}].zip`;  | 
 | 168 | + | 
 | 169 | +    printProgress(`Archiving results from: ${resultsFolder}`);  | 
 | 170 | +    await zip(resultsFolder, archiveName);  | 
 | 171 | +    printSuccess("Archive created");  | 
 | 172 | + | 
 | 173 | +    const ossArtifactZip = join(resultsFolder, archiveName);  | 
 | 174 | +    const downloadsFolder = join(homedir(), "Downloads");  | 
 | 175 | +    const finalPath = join(downloadsFolder, finalName);  | 
 | 176 | + | 
 | 177 | +    printProgress(`Moving to: ${finalPath}`);  | 
 | 178 | +    mv(ossArtifactZip, finalPath);  | 
 | 179 | +    printSuccess("Archive moved to Downloads folder");  | 
 | 180 | + | 
 | 181 | +    printProgress("Cleaning up temporary files...");  | 
 | 182 | +    rm("-rf", tmpFolder);  | 
 | 183 | +    printSuccess("Cleanup completed");  | 
 | 184 | + | 
 | 185 | +    console.log(chalk.bold.green(`\n🎉 Success! Output file:`));  | 
 | 186 | +    console.log(chalk.cyan(`   ${finalPath}\n`));  | 
 | 187 | +}  | 
 | 188 | + | 
 | 189 | +async function computeHash(filepath: string): Promise<string> {  | 
 | 190 | +    const input = fs.createReadStream(filepath);  | 
 | 191 | +    const hash = crypto.createHash("sha256");  | 
 | 192 | +    await pipeline(input, hash);  | 
 | 193 | +    return hash.digest("hex");  | 
 | 194 | +}  | 
 | 195 | + | 
 | 196 | +async function createFolderStructure(name: string): Promise<[string, string]> {  | 
 | 197 | +    const tmpFolder = await mkdtemp(join(tmpdir(), "tmp_OSS_Clearance_Artifacts_"));  | 
 | 198 | +    const artifactsFolder = join(tmpFolder, "SBOM_GENERATOR", name);  | 
 | 199 | +    await mkdir("-p", artifactsFolder);  | 
 | 200 | +    return [tmpFolder, join(artifactsFolder, `${name}.mpk`)];  | 
 | 201 | +}  | 
 | 202 | + | 
 | 203 | +// ============================================================================  | 
 | 204 | +// Main Function  | 
 | 205 | +// ============================================================================  | 
 | 206 | + | 
 | 207 | +async function main(): Promise<void> {  | 
 | 208 | +    printHeader("OSS Clearance Artifacts Preparation Tool");  | 
 | 209 | + | 
 | 210 | +    try {  | 
 | 211 | +        // Step 1: Verify authentication  | 
 | 212 | +        await verifyGitHubAuth();  | 
 | 213 | + | 
 | 214 | +        // Step 2: Select release  | 
 | 215 | +        const release = await selectRelease();  | 
 | 216 | + | 
 | 217 | +        // Step 3: Find MPK asset  | 
 | 218 | +        const mpkAsset = await findAndValidateMpkAsset(release);  | 
 | 219 | + | 
 | 220 | +        // Prepare folder structure  | 
 | 221 | +        const [tmpFolder, downloadPath] = await createFolderStructure(release.name);  | 
 | 222 | +        printInfo(`Working directory: ${tmpFolder}`);  | 
 | 223 | + | 
 | 224 | +        // Step 4: Download and verify  | 
 | 225 | +        const fileHash = await downloadAndVerifyAsset(mpkAsset, downloadPath);  | 
 | 226 | + | 
 | 227 | +        // Step 5: Run SBOM Generator  | 
 | 228 | +        await runSbomGenerator(tmpFolder);  | 
 | 229 | + | 
 | 230 | +        // Step 6: Create final archive  | 
 | 231 | +        await createOutputArchive(tmpFolder, release.name, fileHash);  | 
 | 232 | +    } catch (error) {  | 
 | 233 | +        console.log("\n" + chalk.bold.red("═".repeat(60)));  | 
 | 234 | +        printError(`Process failed: ${(error as Error).message}`);  | 
 | 235 | +        console.log(chalk.bold.red("═".repeat(60)) + "\n");  | 
 | 236 | +        process.exit(1);  | 
 | 237 | +    }  | 
 | 238 | +}  | 
 | 239 | + | 
 | 240 | +// ============================================================================  | 
 | 241 | +// Entry Point  | 
 | 242 | +// ============================================================================  | 
 | 243 | + | 
 | 244 | +main().catch(e => {  | 
 | 245 | +    console.error(chalk.red("\n💥 Unexpected error:"), e);  | 
 | 246 | +    process.exit(1);  | 
 | 247 | +});  | 
0 commit comments