Skip to content
Open
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
85 changes: 85 additions & 0 deletions plugins/git-proxy-plugin-samples/customSecretScanner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Copyright 2026 GitProxy Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* This sample plugin scans for secrets in the diff of a git push.
*/

// Peer dependencies; it's expected that these deps exist on Node module path if you've installed @finos/git-proxy
import { PushActionPlugin, PushPhase, PushPluginOptions } from '@finos/git-proxy/plugin';
import { Action, Step } from '@finos/git-proxy/proxy/actions';
import { Request } from 'express';
import parseDiff from 'parse-diff';

const RULES = [
{ name: 'AWS access key', re: /AKIA[0-9A-Z]{16}/g },
{ name: 'Private key block', re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g },
{ name: 'Assigned secret', re: /(api[_-]?key|token|password)\s*[:=]\s*['"][^'"]{8,}/gi },
];

class CustomSecretScanner extends PushActionPlugin {
constructor() {
super(exec, pluginOptions);
}
}

const pluginOptions: PushPluginOptions = {
phase: PushPhase.AFTER_DIFF, // When to execute the plugin within default chain steps
displayName: 'CustomSecretScanner', // Display name for the plugin
isCollectible: true, // If true, the chain will keep running even if plugin returns an error
chains: ['branch', 'tag'], // Which chains to execute the plugin on
};

async function exec(req: Request, action: Action) {
const step = new Step('CustomSecretScanner');
const diff = action.steps.find((s) => s.stepName === 'diff')?.content;

if (!diff) {
step.log('no diff available; skipping scan');
action.addStep(step);
return action;
}

const findings = findSecrets(diff);
if (findings.length > 0) {
const report = findings.map((f, i) => `${i + 1}. ${f.rule} in ${f.file}:${f.line}`).join('\n');
step.error = true;
step.setError(`\n\nPush blocked: possible secrets detected.\n\n${report}\n`);
}

action.addStep(step);
return action;
}

const findSecrets = (diff: string): { rule: string; file?: string; line: number }[] =>
parseDiff(diff).flatMap((file) =>
file.chunks.flatMap((chunk) =>
chunk.changes
.filter((c) => c.type === 'add') // filter for newly added lines
.flatMap((c) =>
RULES.flatMap((rule) =>
[...c.content.matchAll(rule.re)].map(() => ({
rule: rule.name,
file: file.to || file.from,
line: c.ln,
})),
),
),
),
);

// Default exports are supported and will be loaded by the plugin loader
export default new CustomSecretScanner();
2 changes: 1 addition & 1 deletion plugins/git-proxy-plugin-samples/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* ES modules to demonstrate the use of ESM in plugins.
*/

// Peer dependencies; its expected that these deps exist on Node module path if you've installed @finos/git-proxy
// Peer dependencies; it's expected that these deps exist on Node module path if you've installed @finos/git-proxy
import { PullActionPlugin } from '@finos/git-proxy/plugin';
import { Step } from '@finos/git-proxy/proxy/actions';

Expand Down
2 changes: 1 addition & 1 deletion plugins/git-proxy-plugin-samples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@
"express": "^5.2.1"
},
"peerDependencies": {
"@finos/git-proxy": "^2.0.0"
"@finos/git-proxy": "^2.1.0"
}
}
112 changes: 99 additions & 13 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Module from 'node:module';

import { Action } from './proxy/actions';
import { handleErrorAndLog } from './utils/errors';
import { PullPhase, PushChainName, PushPhase } from './proxy/processors/types';

/* eslint-disable @typescript-eslint/no-unused-expressions */
('use strict');
Expand Down Expand Up @@ -192,11 +193,68 @@ class ProxyPlugin {
}

/**
* A plugin which executes a function when receiving a git push request.
* Options for all ActionPlugin instances.
* @property {boolean} isCollectible - If true, the plugin will not stop the chain if it fails. Errors will be collected
* and reported at the end of the chain. Useful for plugins that are not critical to the success of the operation.
* @property {string} displayName - The name of the plugin which is used for user-facing progress reporting. Optional.
* @property {PushPhase | PullPhase} phase - The phase of the action chain where the plugin will be executed. Optional.
*/
interface ActionPluginOptions {
readonly isCollectible?: boolean;
readonly displayName?: string;
readonly phase?: PushPhase | PullPhase;
}

/**
* Options for PushActionPlugin instances, extended from {ActionPluginOptions}.
* @property {PushPhase} phase - The phase of the *push* action chain where the plugin will be executed. Defaults to {PushPhase.AFTER_PERMISSIONS}.
* @property {PushChainName[]} chains - The push operations where the plugin will be executed. Optional, defaults to all push operations.
*/
interface PushPluginOptions extends ActionPluginOptions {
readonly phase?: PushPhase;
readonly chains?: PushChainName[];
}

/**
* Options for PullActionPlugin instances, extended from {ActionPluginOptions}.
* @property {PullPhase} phase - The phase of the *pull* action chain where the plugin will be executed. Defaults to {PullPhase.AFTER_AUTHORISATION}.
*/
interface PullPluginOptions extends ActionPluginOptions {
readonly phase?: PullPhase;
}

/**
* Base class for all action plugins (executed as part of the action chain for
* `git push` or `git pull` operations).
*/
class PushActionPlugin extends ProxyPlugin {
isGitProxyPushActionPlugin: boolean;
export abstract class ActionPlugin extends ProxyPlugin {
exec: (req: Request, action: Action) => Promise<Action>;
readonly isCollectible: boolean;
readonly displayName?: string;
readonly phase: PushPhase | PullPhase;

/**
* Parent constructor for all ActionPlugin instances. Do not use this constructor directly.
*/
constructor(
exec: (req: Request, action: Action) => Promise<Action>,
options: ActionPluginOptions & { phase: PushPhase | PullPhase },
) {
super();
this.exec = exec;
this.isCollectible = options.isCollectible ?? false;
this.displayName = options.displayName;
this.phase = options.phase;
}
}

/**
* A plugin which executes a function when receiving a git push request.
*/
class PushActionPlugin extends ActionPlugin {
isGitProxyPushActionPlugin = true;
declare readonly phase: PushPhase;
declare readonly chains?: PushChainName[];

/**
* Wrapper class which contains at least one function executed as part of the action chain for git push operations.
Expand All @@ -211,20 +269,30 @@ class PushActionPlugin extends ProxyPlugin {
* - Takes in an Express Request object as the first parameter (`req`).
* - Takes in an Action object as the second parameter (`action`).
* - Returns a Promise that resolves to an Action.
*
* @param {PushPluginOptions} options - An object containing the following properties:
* - {boolean} isCollectible - If true, the plugin will not stop the chain if it fails. Errors will be collected
* and reported at the end of the chain. Useful for plugins that are not critical to the success of the operation.
* - {string} displayName - The name of the plugin which is used for user-facing progress reporting. Optional.
* - {PushPhase} phase - The phase of the *push* action chain where the plugin will be executed. Optional, defaults to {PushPhase.AFTER_PERMISSIONS}.
* - {PushChainName[]} chains - The push operations where the plugin will be executed. Optional, defaults to all push operations.
*/
constructor(exec: (req: Request, action: Action) => Promise<Action>) {
super();
constructor(
exec: (req: Request, action: Action) => Promise<Action>,
options: PushPluginOptions = {},
) {
super(exec, { ...options, phase: options.phase ?? PushPhase.AFTER_PERMISSIONS });
this.isGitProxyPushActionPlugin = true;
this.exec = exec;
this.chains = options.chains;
}
}

/**
* A plugin which executes a function when receiving a git fetch request.
*/
class PullActionPlugin extends ProxyPlugin {
isGitProxyPullActionPlugin: boolean;
exec: (req: Request, action: Action) => Promise<Action>;
class PullActionPlugin extends ActionPlugin {
isGitProxyPullActionPlugin = true;
declare readonly phase: PullPhase;

/**
* Wrapper class which contains at least one function executed as part of the action chain for git pull operations.
Expand All @@ -239,12 +307,30 @@ class PullActionPlugin extends ProxyPlugin {
* - Takes in an Express Request object as the first parameter (`req`).
* - Takes in an Action object as the second parameter (`action`).
* - Returns a Promise that resolves to an Action.
*
* @param {PushPluginOptions} options - An object containing the following properties:
* - {boolean} isCollectible - If true, the plugin will not stop the chain if it fails. Errors will be collected
* and reported at the end of the chain. Useful for plugins that are not critical to the success of the operation.
* - {string} displayName - The name of the plugin which is used for user-facing progress reporting. Optional.
* - {PullPhase} phase - The phase of the *pull* action chain where the plugin will be executed. Optional, defaults to {PullPhase.AFTER_AUTHORISATION}.
*/
constructor(exec: (req: Request, action: Action) => Promise<Action>) {
super();
constructor(
exec: (req: Request, action: Action) => Promise<Action>,
options: PullPluginOptions = {},
) {
super(exec, { ...options, phase: options.phase ?? PullPhase.AFTER_AUTHORISATION });
this.isGitProxyPullActionPlugin = true;
this.exec = exec;
}
}

export { PluginLoader, PushActionPlugin, PullActionPlugin, isCompatiblePlugin };
export {
PluginLoader,
PushActionPlugin,
PullActionPlugin,
isCompatiblePlugin,
PushPhase,
PullPhase,
PushChainName,
PushPluginOptions,
PullPluginOptions,
};
Loading
Loading