Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
.vscode
*.iml

node_modules/.package-lock.json
node_modules
__tests__/runner/*

# Vim
*~
*.swp

# IOS
*.DS_Store
*.DS_Store
3 changes: 0 additions & 3 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npm i
npm prune --production
git add node_modules
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ inputs:
description: "By default, if the workflow completes with collected build-info that has not been published using the jf rt build-publish command, the build-info will be automatically published to Artifactory. Set this to true to disable the automatic publication of build-info at the end of the workflow."
default: "false"
required: false
disable-auto-evidence-collection:
description: "Set to true to disable the automatic collection of evidence at the end of the workflow."
default: "false"
required: false
custom-server-id:
description: "Custom JFrog CLI configuration server ID to use instead of the default one generated by the action."
required: false
Expand Down
21 changes: 16 additions & 5 deletions lib/cleanup.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkConnectionToArtifactory = checkConnectionToArtifactory;
const core = __importStar(require("@actions/core"));
const utils_1 = require("./utils");
const job_summary_1 = require("./job-summary");
const evidence_collection_1 = require("./evidence-collection");
function cleanup() {
return __awaiter(this, void 0, void 0, function* () {
if (yield shouldSkipCleanup()) {
return;
}
// Run post tasks related to Build Info (auto build publish, job summary)
// Run post tasks related to Build Info (auto build publish, evidence collection, job summary)
yield buildInfoPostTasks();
// Cleanup JFrog CLI servers configuration
try {
Expand All @@ -59,17 +61,19 @@ function cleanup() {
* Executes post tasks related to build information.
*
* This function performs several tasks after the main build process:
* 1. Checks if auto build publish and job summary are disabled.
* 1. Checks if auto build publish, evidence collection, and job summary are disabled.
* 2. Verifies connection to JFrog Artifactory.
* 3. Collects and publishes build information if needed.
* 4. Generates a job summary if required.
* 4. Collects evidences if enabled.
* 5. Generates a job summary if required.
*/
function buildInfoPostTasks() {
return __awaiter(this, void 0, void 0, function* () {
const disableAutoBuildPublish = core.getBooleanInput(utils_1.Utils.AUTO_BUILD_PUBLISH_DISABLE);
const disableAutoEvidenceCollection = core.getBooleanInput(utils_1.Utils.AUTO_EVIDENCE_COLLECTION_DISABLE);
const disableJobSummary = core.getBooleanInput(utils_1.Utils.JOB_SUMMARY_DISABLE) || !job_summary_1.JobSummary.isJobSummarySupported();
if (disableAutoBuildPublish && disableJobSummary) {
core.info(`Both auto-build-publish and job-summary are disabled. Skipping Build Info post tasks.`);
if (disableAutoBuildPublish && disableAutoEvidenceCollection && disableJobSummary) {
core.info(`All post tasks (auto-build-publish, evidence collection, and job-summary) are disabled. Skipping Build Info post tasks.`);
return;
}
// Check connection to Artifactory before proceeding with build info post tasks
Expand All @@ -83,6 +87,13 @@ function buildInfoPostTasks() {
else {
core.info('Auto build info publish is disabled. Skipping auto build info collection and publishing');
}
// Collect evidences if not disabled
if (!disableAutoEvidenceCollection) {
yield (0, evidence_collection_1.collectEvidences)();
}
else {
core.info('Auto evidence collection is disabled. Skipping evidence collection');
}
// Generate job summary if not disabled and the JFrog CLI version supports it
if (!disableJobSummary) {
yield generateJobSummary();
Expand Down
165 changes: 165 additions & 0 deletions lib/evidence-collection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.collectEvidences = collectEvidences;
const core = __importStar(require("@actions/core"));
const utils_1 = require("./utils");
const http_client_1 = require("@actions/http-client");
const fs_1 = require("fs");
const path = __importStar(require("path"));
/**
* Collects evidences from the current workflow.
* This function first checks if evidence collection is supported by the Artifactory server.
*/
function collectEvidences() {
return __awaiter(this, void 0, void 0, function* () {
try {
core.startGroup('Collecting evidences');
// Check if evidence collection is supported by the server
const evidenceConfig = yield getEvidenceConfiguration();
if (!evidenceConfig.external_evidence_collection_supported) {
core.info('Evidence collection is not supported by this Artifactory server. Skipping evidence collection.');
return;
}
core.info(`Evidence collection is supported. Maximum file size: ${evidenceConfig.max_evidence_file_size_mb} MB`);
// Read sigstore bundle file paths and create evidence for each
yield createEvidenceFromSigstoreBundles(evidenceConfig.max_evidence_file_size_mb);
}
catch (error) {
core.warning('Failed while attempting to collect evidences: ' + error);
}
finally {
core.endGroup();
}
});
}
/**
* Checks if evidence collection is supported by the Artifactory server.
* @returns EvidenceConfigResponse with support status and max file size
*/
function getEvidenceConfiguration() {
return __awaiter(this, void 0, void 0, function* () {
const credentials = utils_1.Utils.collectJfrogCredentialsFromEnvVars();
if (!credentials.jfrogUrl) {
throw new Error('JF_URL is required to check evidence support');
}
// Get access token for authentication
let accessToken = credentials.accessToken;
if (!accessToken && credentials.oidcProviderName) {
// Import OidcUtils dynamically to avoid circular dependency
const { OidcUtils } = yield Promise.resolve().then(() => __importStar(require('./oidc-utils')));
accessToken = yield OidcUtils.exchangeOidcToken(credentials);
}
if (!accessToken) {
throw new Error('No access token available for authentication');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are supporting Basic Auth with JF_USER and JF_PASSWORD we should give a meaningful error in this case.

}
// Remove trailing slash from jfrogUrl to avoid double slashes when appending the API path
const url = `${credentials.jfrogUrl.replace(/\/$/, '')}/evidence/api/v1/config/`;
const httpClient = new http_client_1.HttpClient();
const headers = {
'Authorization': `Bearer ${accessToken}`,
};
core.debug(`Getting evidence configuration at: ${url}`);
let response;
let body;
try {
response = yield httpClient.get(url, headers);
body = yield response.readBody();
}
catch (error) {
core.warning(`Failed to get evidence configuration (network error or server unavailable): ${error}`);
return { external_evidence_collection_supported: false, max_evidence_file_size_mb: 0 };
}
if (response.message.statusCode !== 200) {
core.warning(`Failed to get evidence configuration. Status: ${response.message.statusCode}, Response: ${body}`);
return { external_evidence_collection_supported: false, max_evidence_file_size_mb: 0 };
}
try {
const config = JSON.parse(body);
return config;
}
catch (error) {
core.warning(`Failed to parse evidence config response: ${error}`);
return { external_evidence_collection_supported: false, max_evidence_file_size_mb: 0 };
}
});
}
/**
* Reads sigstore bundle file paths and creates evidence for each file.
* Reads from ${RUNNER_TEMP}/created_attestation_paths.txt
* @param maxFileSizeMB Maximum allowed file size in MB
*/
function createEvidenceFromSigstoreBundles(maxFileSizeMB) {
return __awaiter(this, void 0, void 0, function* () {
const runnerTemp = process.env.RUNNER_TEMP;
if (!runnerTemp) {
core.warning('RUNNER_TEMP environment variable is not set. Skipping evidence creation.');
return;
}
const attestationPathsFile = path.join(runnerTemp, 'created_attestation_paths.txt');
try {
// Check if the file exists
yield fs_1.promises.access(attestationPathsFile);
}
catch (error) {
core.info(`No attestation paths file found. Skipping evidence creation. Searched for: ${attestationPathsFile}. Error: ${error}`);
return;
}
// Read the file content
core.info(`Reading attestation paths file: ${attestationPathsFile}`);
const fileContent = yield fs_1.promises.readFile(attestationPathsFile, 'utf8');
const filePaths = fileContent.split('\n').filter(line => line.trim() !== '');
if (filePaths.length === 0) {
core.info('No sigstore bundle files found in attestation paths file.');
return;
}
core.info(`Found ${filePaths.length} sigstore bundle file(s) to process.`);
for (const filePath of filePaths) {
try {
const fileStats = yield fs_1.promises.stat(filePath);
const fileSizeMB = fileStats.size / (1024 * 1024); // Convert bytes to MB
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment in config API.
If we will use it in Azure for example I don't think we want to convert each time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think converting makes sense. It leaves the API readable to humans

if (fileSizeMB > maxFileSizeMB) {
core.warning(`Skipping ${filePath}: File size (${fileSizeMB.toFixed(2)} MB) exceeds maximum allowed size (${maxFileSizeMB} MB)`);
continue;
}
core.info(`Creating evidence for: ${filePath}`);
const output = yield utils_1.Utils.runCliAndGetOutput(['evd', 'create', '--sigstore-bundle', filePath]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens in the case JF_PROJECT is defined? don't we want to use --project here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see that we pass project in the jf cli:

func NewCreateEvidenceCustom(serverDetails *config.ServerDetails, predicateFilePath, predicateType, markdownFilePath, key, keyId, subjectRepoPath,
	subjectSha256, sigstoreBundlePath, providerId string) evidence.Command {
	return &createEvidenceCustom{
		createEvidenceBase: createEvidenceBase{
			serverDetails:     serverDetails,
			predicateFilePath: predicateFilePath,
			predicateType:     predicateType,
			providerId:        providerId,
			markdownFilePath:  markdownFilePath,
			key:               key,
			keyId:             keyId,
		},
		subjectRepoPath:    subjectRepoPath,
		subjectSha256:      subjectSha256,
		sigstoreBundlePath: sigstoreBundlePath,
	}
}

Am I missing something?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need to change it but good for now

core.info(`Evidence created successfully for ${filePath}: ${output}`);
}
catch (error) {
core.warning(`Failed to create evidence for ${filePath}: ${error}`);
}
}
});
}
2 changes: 2 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -471,5 +471,7 @@ Utils.OIDC_INTEGRATION_PROVIDER_NAME = 'oidc-provider-name';
Utils.JOB_SUMMARY_DISABLE = 'disable-job-summary';
// Disable auto build info publish feature flag
Utils.AUTO_BUILD_PUBLISH_DISABLE = 'disable-auto-build-publish';
// Disable auto evidence collection feature flag
Utils.AUTO_EVIDENCE_COLLECTION_DISABLE = 'disable-auto-evidence-collection';
// Custom server ID input
Utils.CUSTOM_SERVER_ID = 'custom-server-id';
Loading
Loading