├── .env.production ├── .husky ├── pre-commit └── commit-msg ├── website ├── static │ ├── css │ │ └── custom.css │ └── img │ │ ├── logo.png │ │ ├── finos │ │ ├── finos.png │ │ ├── finos-blue.png │ │ └── finos-white.png │ │ ├── github-mark.png │ │ ├── github-mark-white.png │ │ └── favicon │ │ └── favicon-finos.ico ├── src │ ├── pages │ │ ├── styles.module.css │ │ └── testimonials.js │ └── components │ │ └── avatar.js ├── .npmrc ├── docs │ ├── index.mdx │ ├── usage.mdx │ ├── installation.mdx │ └── configuration │ │ └── pre-receive.mdx ├── package.json ├── sidebars.js └── README.md ├── test ├── fixtures │ ├── proxy.config.valid-1.json │ ├── baz.js │ ├── captured-push.bin │ ├── test-package │ │ ├── package.json │ │ ├── default-export.js │ │ ├── esm-export.js │ │ ├── subclass.js │ │ ├── esm-subclass.js │ │ ├── multiple-export.js │ │ └── esm-multiple-export.js │ ├── proxy.config.invalid-2.json │ ├── proxy.config.invalid-1.json │ ├── proxy.config.valid-2.json │ ├── gitleaks-config.toml │ └── captured-push.bin-README.md ├── preReceive │ ├── mock │ │ └── repo │ │ │ └── test-repo │ │ │ └── Readme.md │ └── pre-receive-hooks │ │ ├── always-exit-99.sh │ │ ├── always-exit-0.sh │ │ ├── always-exit-1.sh │ │ └── always-exit-2.sh ├── proxyURL.test.ts ├── processors │ └── clearBareClone.test.ts ├── ui │ └── apiBase.test.ts ├── services │ └── routes │ │ └── users.test.ts ├── db │ ├── mongo │ │ └── repo.test.ts │ └── db.test.ts └── testAuthMethods.test.ts ├── .env.development ├── experimental ├── license-inventory │ ├── .gitignore │ ├── src │ │ ├── db │ │ │ ├── types.ts │ │ │ ├── collections.ts │ │ │ ├── index.ts │ │ │ ├── connect.ts │ │ │ └── schemas │ │ │ │ └── license │ │ │ │ ├── license.ts │ │ │ │ └── license.test.ts │ │ ├── test │ │ │ ├── utils │ │ │ │ └── config.ts │ │ │ ├── mock │ │ │ │ └── db.ts │ │ │ ├── setupFile.ts │ │ │ ├── globalTeardown.ts │ │ │ └── globalSetup.ts │ │ ├── logger.ts │ │ ├── routes │ │ │ └── api │ │ │ │ ├── index.ts │ │ │ │ └── v0 │ │ │ │ └── index.ts │ │ ├── env.ts │ │ ├── types.ts │ │ ├── services │ │ │ └── data │ │ │ │ ├── index.ts │ │ │ │ └── license.ts │ │ ├── server.ts │ │ └── app.ts │ ├── tsconfig.publish.json │ ├── nodemon.json │ ├── dev │ │ ├── promtail.yaml │ │ ├── prometheus.yaml │ │ ├── tempo.yaml │ │ ├── grafana │ │ │ └── datasources.yaml │ │ ├── loki.yaml │ │ └── otel-collector-config.yaml │ ├── jest.config.ts │ ├── eslint.config.mjs │ ├── tsconfig.json │ └── README.md └── li-cli │ ├── tsconfig.publish.json │ ├── jest.config.ts │ ├── tsconfig.json │ ├── package.json │ └── src │ ├── cli.ts │ └── lib │ ├── spdx.ts │ └── inventory.ts ├── docs └── img │ ├── demo.png │ └── logo.png ├── public ├── favicon.ico ├── apple-icon.png └── manifest.json ├── commitlint.config.js ├── netlify.toml ├── src ├── types │ ├── images.d.ts │ └── passport-activedirectory.d.ts ├── ui │ ├── assets │ │ ├── img │ │ │ └── git-proxy.png │ │ └── jss │ │ │ └── material-dashboard-react │ │ │ ├── components │ │ │ ├── cardBodyStyle.ts │ │ │ ├── cardIconStyle.js │ │ │ ├── cardAvatarStyle.js │ │ │ ├── cardStyle.ts │ │ │ ├── typographyStyle.js │ │ │ ├── cardFooterStyle.js │ │ │ ├── tasksStyle.js │ │ │ ├── footerStyle.ts │ │ │ ├── customInputStyle.js │ │ │ └── tableStyle.js │ │ │ ├── cardImagesStyles.js │ │ │ ├── layouts │ │ │ ├── rtlStyle.js │ │ │ └── dashboardStyle.ts │ │ │ ├── tooltipStyle.js │ │ │ ├── views │ │ │ ├── iconsStyle.js │ │ │ └── dashboardStyle.js │ │ │ └── checkboxAdnRadioStyle.js │ ├── components │ │ ├── Search │ │ │ ├── Search.css │ │ │ └── Search.tsx │ │ ├── UserLink │ │ │ └── UserLink.tsx │ │ ├── Typography │ │ │ ├── Info.jsx │ │ │ ├── Danger.jsx │ │ │ ├── Muted.jsx │ │ │ ├── Warning.jsx │ │ │ ├── Primary.jsx │ │ │ ├── Success.jsx │ │ │ └── Quote.jsx │ │ ├── Grid │ │ │ ├── GridItem.tsx │ │ │ └── GridContainer.tsx │ │ ├── Pagination │ │ │ ├── Pagination.css │ │ │ └── Pagination.tsx │ │ ├── Card │ │ │ ├── CardBody.tsx │ │ │ ├── CardAvatar.tsx │ │ │ ├── CardIcon.tsx │ │ │ ├── Card.tsx │ │ │ ├── CardFooter.tsx │ │ │ └── CardHeader.tsx │ │ ├── Filtering │ │ │ └── Filtering.css │ │ ├── Footer │ │ │ └── Footer.tsx │ │ ├── CustomButtons │ │ │ └── Button.tsx │ │ ├── RouteGuard │ │ │ └── RouteGuard.tsx │ │ └── Snackbar │ │ │ └── SnackbarContent.tsx │ ├── apiBase.ts │ ├── views │ │ ├── RepoList │ │ │ ├── RepoList.tsx │ │ │ └── Components │ │ │ │ └── TabList.tsx │ │ ├── UserList │ │ │ ├── UserList.tsx │ │ │ └── Components │ │ │ │ └── TabList.tsx │ │ ├── PushDetails │ │ │ └── components │ │ │ │ └── Diff.tsx │ │ └── Extras │ │ │ ├── NotFound.jsx │ │ │ └── NotAuthorized.jsx │ ├── context.ts │ ├── auth │ │ └── AuthProvider.tsx │ └── services │ │ ├── config.ts │ │ └── auth.ts ├── proxy │ ├── processors │ │ ├── pre-processor │ │ │ ├── index.ts │ │ │ └── parseAction.ts │ │ ├── index.ts │ │ ├── constants.ts │ │ ├── push-action │ │ │ ├── audit.ts │ │ │ ├── clearBareClone.ts │ │ │ ├── blockForAuth.ts │ │ │ ├── checkIfWaitingAuth.ts │ │ │ ├── checkRepoInAuthorisedList.ts │ │ │ ├── checkEmptyBranch.ts │ │ │ ├── index.ts │ │ │ ├── writePack.ts │ │ │ ├── getDiff.ts │ │ │ ├── pullRemote.ts │ │ │ └── checkAuthorEmails.ts │ │ └── types.ts │ └── actions │ │ ├── index.ts │ │ ├── autoActions.ts │ │ └── Step.ts ├── db │ ├── file │ │ ├── helper.ts │ │ └── index.ts │ ├── mongo │ │ └── index.ts │ └── helper.ts ├── service │ ├── routes │ │ ├── healthcheck.ts │ │ ├── home.ts │ │ ├── config.ts │ │ ├── utils.ts │ │ ├── index.ts │ │ └── users.ts │ ├── urls.ts │ └── passport │ │ ├── types.ts │ │ ├── index.ts │ │ └── ldaphelper.ts ├── config │ ├── env.ts │ ├── file.ts │ └── types.ts └── index.tsx ├── CODE_OF_CONDUCT.md ├── nyc.config.js ├── NOTICE ├── jsfh.config.json ├── scripts ├── undo-build.sh ├── fix-shebang.sh ├── add-banner.ts ├── prepare.js └── doc-schema.js ├── codecov.yml ├── .npmignore ├── LICENSE.spdx ├── .prettierrc ├── certs ├── create-cert.sh └── cert.pem ├── .vscode └── settings.json ├── tsconfig.publish.json ├── cypress.config.js ├── vite.config.ts ├── plugins └── git-proxy-plugin-samples │ ├── package.json │ ├── index.js │ ├── README.md │ └── example.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── dependency-review.yml │ ├── unused-dependencies.yml │ ├── sample-publish.yml │ ├── npm.yml │ ├── experimental-inventory-cli-publish.yml │ ├── lint.yml │ ├── pr-lint.yml │ ├── experimental-inventory-publish.yml │ ├── experimental-inventory-ci.yml │ └── codeql.yml └── release-drafter.yml ├── tsconfig.json ├── cypress ├── support │ └── e2e.js └── e2e │ └── login.cy.js ├── packages └── git-proxy-cli │ ├── package.json │ ├── tsconfig.json │ └── test │ └── testCli.proxy.config.json ├── renovate.json ├── vitest.config.ts ├── SECURITY.md └── index.ts /.env.production: -------------------------------------------------------------------------------- 1 | VITE_API_URI= -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged -------------------------------------------------------------------------------- /website/static/css/custom.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/src/pages/styles.module.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /test/fixtures/proxy.config.valid-1.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | VITE_API_URI=http://localhost:8080 2 | -------------------------------------------------------------------------------- /experimental/license-inventory/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /test/preReceive/mock/repo/test-repo/Readme.md: -------------------------------------------------------------------------------- 1 | Mock repository. 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit ${1} && npm run lint 2 | -------------------------------------------------------------------------------- /test/preReceive/pre-receive-hooks/always-exit-99.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exit 99 -------------------------------------------------------------------------------- /test/fixtures/baz.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | foo: 'bar', 3 | baz: {}, 4 | }; 5 | -------------------------------------------------------------------------------- /docs/img/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finos/git-proxy/HEAD/docs/img/demo.png -------------------------------------------------------------------------------- /docs/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finos/git-proxy/HEAD/docs/img/logo.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finos/git-proxy/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finos/git-proxy/HEAD/public/apple-icon.png -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF . ../website/" 3 | -------------------------------------------------------------------------------- /src/types/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' { 2 | const url: string; 3 | export default url; 4 | } 5 | -------------------------------------------------------------------------------- /website/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finos/git-proxy/HEAD/website/static/img/logo.png -------------------------------------------------------------------------------- /src/ui/assets/img/git-proxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finos/git-proxy/HEAD/src/ui/assets/img/git-proxy.png -------------------------------------------------------------------------------- /test/fixtures/captured-push.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finos/git-proxy/HEAD/test/fixtures/captured-push.bin -------------------------------------------------------------------------------- /website/static/img/finos/finos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finos/git-proxy/HEAD/website/static/img/finos/finos.png -------------------------------------------------------------------------------- /website/static/img/github-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finos/git-proxy/HEAD/website/static/img/github-mark.png -------------------------------------------------------------------------------- /src/proxy/processors/pre-processor/index.ts: -------------------------------------------------------------------------------- 1 | import { exec } from './parseAction'; 2 | 3 | export const parseAction = exec; 4 | -------------------------------------------------------------------------------- /website/static/img/finos/finos-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finos/git-proxy/HEAD/website/static/img/finos/finos-blue.png -------------------------------------------------------------------------------- /website/static/img/finos/finos-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finos/git-proxy/HEAD/website/static/img/finos/finos-white.png -------------------------------------------------------------------------------- /website/static/img/github-mark-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finos/git-proxy/HEAD/website/static/img/github-mark-white.png -------------------------------------------------------------------------------- /src/proxy/actions/index.ts: -------------------------------------------------------------------------------- 1 | import { Action } from './Action'; 2 | import { Step } from './Step'; 3 | 4 | export { Action, Step }; 5 | -------------------------------------------------------------------------------- /website/static/img/favicon/favicon-finos.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finos/git-proxy/HEAD/website/static/img/favicon/favicon-finos.ico -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct for GitProxy 2 | 3 | Please see the [Community Code of Conduct](https://www.finos.org/code-of-conduct). 4 | -------------------------------------------------------------------------------- /src/proxy/processors/index.ts: -------------------------------------------------------------------------------- 1 | import * as pre from './pre-processor'; 2 | import * as push from './push-action'; 3 | 4 | export { pre, push }; 5 | -------------------------------------------------------------------------------- /nyc.config.js: -------------------------------------------------------------------------------- 1 | const opts = { 2 | checkCoverage: true, 3 | lines: 80, 4 | }; 5 | 6 | console.log('nyc config: ', opts); 7 | module.exports = opts; 8 | -------------------------------------------------------------------------------- /test/preReceive/pre-receive-hooks/always-exit-0.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | while read oldrev newrev refname; do 3 | echo "Push allowed to $refname" 4 | done 5 | exit 0 -------------------------------------------------------------------------------- /test/preReceive/pre-receive-hooks/always-exit-1.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | while read oldrev newrev refname; do 3 | echo "Push rejected to $refname" 4 | done 5 | exit 1 -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Git Proxy - FINOS 2 | Copyright 2020 Citigroup 3 | 4 | This product includes software developed at the Fintech Open Source Foundation (https://www.finos.org/). 5 | -------------------------------------------------------------------------------- /jsfh.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template_name": "md_nested", 3 | "show_toc": false, 4 | "template_md_options": { 5 | "show_array_restrictions": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/preReceive/pre-receive-hooks/always-exit-2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | while read oldrev newrev refname; do 3 | echo "Push need manual approve to $refname" 4 | done 5 | exit 2 -------------------------------------------------------------------------------- /test/fixtures/test-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-package", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "@finos/git-proxy": "file:../../.." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /experimental/li-cli/tsconfig.publish.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false 5 | }, 6 | "exclude": ["src/test/**", "src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /scripts/undo-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euxo pipefail 3 | 4 | # Clean build artifacts 5 | 6 | REPO_ROOT="$(git rev-parse --show-toplevel)" 7 | cd "$REPO_ROOT" 8 | 9 | rm -rf dist 10 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/db/types.ts: -------------------------------------------------------------------------------- 1 | import { Types } from 'mongoose'; 2 | 3 | export interface Mongoose { 4 | _id: Types.UUID; 5 | } 6 | 7 | export type Mongoosify = Omit & Mongoose; 8 | -------------------------------------------------------------------------------- /experimental/license-inventory/tsconfig.publish.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false 5 | }, 6 | "exclude": ["src/test/**", "src/**/*.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 80 6 | informational: true 7 | patch: 8 | default: 9 | target: 80 10 | informational: true 11 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/test/utils/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | Memory: typeof process.env.MONGOMS_SYSTEM_BINARY === 'string', 3 | IP: '127.0.0.1', 4 | Port: '27017', 5 | Database: 'testdb', 6 | }; 7 | -------------------------------------------------------------------------------- /scripts/fix-shebang.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | REPO_ROOT="$(git rev-parse --show-toplevel)" 5 | cd "$REPO_ROOT" 6 | 7 | # Replace tsx with node in the shebang 8 | sed -ie '1s/tsx/node/' dist/index.js 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # This file required to override .gitignore when publishing to npm 2 | src/ 3 | tests/ 4 | *.test.ts 5 | 6 | tsconfig.json 7 | jest.config.js 8 | .eslintrc.js 9 | .prettierrc 10 | 11 | website/ 12 | plugins/ 13 | -------------------------------------------------------------------------------- /test/fixtures/proxy.config.invalid-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "authorisedList": [ 3 | { 4 | "project": "finos", 5 | "name": "git-proxy", 6 | "link": "https://www.github.com/finos/git-proxy" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE.spdx: -------------------------------------------------------------------------------- 1 | SPDXVersion: SPDX-2.0 2 | DataLicense: CC0-1.0 3 | Creator: Citigroup 4 | PackageName: Git Proxy 5 | PackageOriginator: Citigroup 6 | PackageHomePage: https://github.com/finos/git-proxy 7 | PackageLicenseDeclared: Apache-2.0 8 | -------------------------------------------------------------------------------- /test/fixtures/proxy.config.invalid-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "invalidProperty": [ 3 | { 4 | "project": "finos", 5 | "name": "git-proxy", 6 | "url": "https://www.github.com/finos/git-proxy.git" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "jsxSingleQuote": true, 8 | "bracketSpacing": true, 9 | "arrowParens": "always" 10 | } 11 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/db/collections.ts: -------------------------------------------------------------------------------- 1 | import { model } from 'mongoose'; 2 | import { licenseSchema } from './schemas/license/license'; 3 | 4 | // licenses collection 5 | export const License = model('License', licenseSchema); 6 | -------------------------------------------------------------------------------- /src/db/file/helper.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync } from 'fs'; 2 | 3 | export const getSessionStore = (): undefined => undefined; 4 | export const initializeFolders = () => { 5 | if (!existsSync('./.data/db')) mkdirSync('./.data/db', { recursive: true }); 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixtures/test-package/default-export.js: -------------------------------------------------------------------------------- 1 | const { PushActionPlugin } = require('@finos/git-proxy/plugin'); 2 | 3 | // test default export 4 | module.exports = new PushActionPlugin(async (req, action) => { 5 | console.log('Dummy plugin: ', action); 6 | return action; 7 | }); 8 | -------------------------------------------------------------------------------- /test/fixtures/test-package/esm-export.js: -------------------------------------------------------------------------------- 1 | import { PushActionPlugin } from '@finos/git-proxy/plugin'; 2 | 3 | // test default export (ESM syntax) 4 | export default new PushActionPlugin(async (req, action) => { 5 | console.log('Dummy plugin: ', action); 6 | return action; 7 | }); 8 | -------------------------------------------------------------------------------- /src/service/routes/healthcheck.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | 3 | const router = express.Router(); 4 | 5 | router.get('/', (_req: Request, res: Response) => { 6 | res.send({ 7 | message: 'ok', 8 | }); 9 | }); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /src/types/passport-activedirectory.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'passport-activedirectory' { 2 | import { Strategy as PassportStrategy } from 'passport'; 3 | class Strategy extends PassportStrategy { 4 | constructor(options: any, verify: (...args: any[]) => void); 5 | } 6 | export = Strategy; 7 | } 8 | -------------------------------------------------------------------------------- /experimental/license-inventory/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src", ".env"], 3 | "ext": "js,ts,json", 4 | "ignore": ["src/**/*.{spec,test}.ts"], 5 | "exec": "ts-node -r tsconfig-paths/register -r @opentelemetry/auto-instrumentations-node/register --transpile-only src/server.ts | pino-pretty" 6 | } 7 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/logger.ts: -------------------------------------------------------------------------------- 1 | import pino from 'pino'; 2 | import pinoCaller from 'pino-caller'; 3 | 4 | const p = pino({ 5 | redact: ['req.headers.host'], 6 | }); 7 | 8 | export const logger = 9 | process.env.NODE_ENV === 'development' ? pinoCaller(p, { relativeTo: __dirname }) : p; 10 | -------------------------------------------------------------------------------- /src/proxy/processors/constants.ts: -------------------------------------------------------------------------------- 1 | export const BRANCH_PREFIX = 'refs/heads/'; 2 | export const EMPTY_COMMIT_HASH = '0000000000000000000000000000000000000000'; 3 | export const FLUSH_PACKET = '0000'; 4 | export const PACK_SIGNATURE = 'PACK'; 5 | export const PACKET_SIZE = 4; 6 | export const GIT_OBJECT_TYPE_COMMIT = 1; 7 | -------------------------------------------------------------------------------- /certs/create-cert.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Generates a self-signed certificate used for the SSL Certificate used by the GitProxy HTTPS endpoint 4 | 5 | # The certificate expires in 10 years (9 May 2034) 6 | openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 3650 -nodes -subj "/C=US/ST=NY/L=New York/O=FINOS/OU=CTI/CN=localhost" 7 | -------------------------------------------------------------------------------- /src/proxy/processors/push-action/audit.ts: -------------------------------------------------------------------------------- 1 | import { writeAudit } from '../../../db'; 2 | import { Action } from '../../actions'; 3 | 4 | const exec = async (req: any, action: Action) => { 5 | if (action.type !== 'pull') { 6 | await writeAudit(action); 7 | } 8 | 9 | return action; 10 | }; 11 | 12 | exec.displayName = 'audit.exec'; 13 | 14 | export { exec }; 15 | -------------------------------------------------------------------------------- /test/fixtures/proxy.config.valid-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "authorisedList": [ 3 | { 4 | "project": "finos", 5 | "name": "git-proxy", 6 | "url": "https://github.com/finos/git-proxy.git" 7 | }, 8 | { 9 | "project": "finos", 10 | "name": "git-proxy-test", 11 | "url": "git@github.com:finos/git-proxy-test.git" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /experimental/license-inventory/dev/promtail.yaml: -------------------------------------------------------------------------------- 1 | positions: 2 | filename: /tmp/positions.yaml 3 | 4 | clients: 5 | - url: http://loki:3100/loki/api/v1/push 6 | 7 | scrape_configs: 8 | - job_name: system 9 | static_configs: 10 | - targets: 11 | - localhost 12 | labels: 13 | job: mongo 14 | __path__: /var/log-mongo/mongo.log* 15 | -------------------------------------------------------------------------------- /src/ui/components/Search/Search.css: -------------------------------------------------------------------------------- 1 | .search-bar { 2 | width: 100%; 3 | max-width: 100%; 4 | margin: 0 auto 20px auto; 5 | } 6 | 7 | .search-input { 8 | width: 100%; 9 | padding: 10px; 10 | font-size: 16px; 11 | border: 1px solid #ccc; 12 | border-radius: 4px; 13 | box-sizing: border-box; 14 | } 15 | 16 | .search-input:focus { 17 | border-color: #007bff; 18 | } 19 | -------------------------------------------------------------------------------- /src/service/routes/home.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | 3 | const router = express.Router(); 4 | 5 | const resource = { 6 | healthcheck: '/api/v1/healthcheck', 7 | push: '/api/v1/push', 8 | auth: '/api/auth', 9 | }; 10 | 11 | router.get('/', (_req: Request, res: Response) => { 12 | res.send(resource); 13 | }); 14 | 15 | export default router; 16 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/routes/api/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import createV0Router from './v0'; 3 | import { LicenseDataService } from '@/services/data'; 4 | 5 | const createRouter = (lds: LicenseDataService) => { 6 | const router = express.Router(); 7 | 8 | router.use('/v0', createV0Router(lds)); 9 | return router; 10 | }; 11 | 12 | export default createRouter; 13 | -------------------------------------------------------------------------------- /test/fixtures/test-package/subclass.js: -------------------------------------------------------------------------------- 1 | const { PushActionPlugin } = require('@finos/git-proxy/plugin'); 2 | 3 | class DummyPlugin extends PushActionPlugin { 4 | constructor(exec) { 5 | super(); 6 | this.exec = exec; 7 | } 8 | } 9 | 10 | // test default export 11 | module.exports = new DummyPlugin(async (req, action) => { 12 | console.log('Dummy plugin: ', action); 13 | return action; 14 | }); 15 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/test/mock/db.ts: -------------------------------------------------------------------------------- 1 | import { LicenseDataService } from '@/services/data'; 2 | import { jest } from '@jest/globals'; 3 | 4 | export const genMockLicenseDataService = (): jest.Mocked => { 5 | return { 6 | create: jest.fn(), 7 | getByUUID: jest.fn(), 8 | patchByUUID: jest.fn(), 9 | deleteByUUID: jest.fn(), 10 | list: jest.fn(), 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /src/ui/components/UserLink/UserLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | interface UserLinkProps { 5 | username: string; 6 | children?: React.ReactNode; 7 | } 8 | 9 | const UserLink: React.FC = ({ username, children }) => { 10 | return {children || username}; 11 | }; 12 | 13 | export default UserLink; 14 | -------------------------------------------------------------------------------- /test/fixtures/test-package/esm-subclass.js: -------------------------------------------------------------------------------- 1 | import { PushActionPlugin } from '@finos/git-proxy/plugin'; 2 | 3 | class DummyPlugin extends PushActionPlugin { 4 | constructor(exec) { 5 | super(); 6 | this.exec = exec; 7 | } 8 | } 9 | 10 | // test default export (ESM syntax) 11 | export default new DummyPlugin(async (req, action) => { 12 | console.log('Dummy plugin: ', action); 13 | return action; 14 | }); 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.suggestionActions.enabled": false, 3 | "editor.tabSize": 2, 4 | "files.eol": "\n", 5 | "debug.console.wordWrap": false, 6 | "editor.wordWrap": "off", 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": "explicit" 9 | }, 10 | "editor.defaultFormatter": "esbenp.prettier-vscode", 11 | "editor.formatOnSave": true, 12 | "cSpell.words": ["Deltafied"] 13 | } 14 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/routes/api/v0/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import createLicensesRouter from './licenses'; 3 | import { LicenseDataService } from '@/services/data'; 4 | 5 | const createRouter = (lds: LicenseDataService) => { 6 | const router = express.Router(); 7 | 8 | router.use('/licenses', createLicensesRouter(lds)); 9 | return router; 10 | }; 11 | 12 | export default createRouter; 13 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/db/index.ts: -------------------------------------------------------------------------------- 1 | import type { Mongoose, Model } from 'mongoose'; 2 | import { licenseSchema, type LicenseSchema } from './schemas/license/license'; 3 | 4 | export class Database { 5 | mongoose: Mongoose; 6 | License: Model; 7 | constructor(mongoose: Mongoose) { 8 | this.mongoose = mongoose; 9 | this.License = mongoose.model('License', licenseSchema); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/ui/apiBase.ts: -------------------------------------------------------------------------------- 1 | const stripTrailingSlashes = (s: string) => s.replace(/\/+$/, ''); 2 | 3 | /** 4 | * The base URL for API requests. 5 | * 6 | * Uses the `VITE_API_URI` environment variable if set, otherwise defaults to the current origin. 7 | * @return {string} The base URL to use for API requests. 8 | */ 9 | export const API_BASE = process.env.VITE_API_URI 10 | ? stripTrailingSlashes(process.env.VITE_API_URI) 11 | : location.origin; 12 | -------------------------------------------------------------------------------- /tsconfig.publish.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "outDir": "./dist", 6 | "rootDir": "./" 7 | }, 8 | "include": ["src/**/*", "index.ts"], 9 | "exclude": [ 10 | "experimental/**", 11 | "plugins/**", 12 | "./dist/**", 13 | "src/ui/**", 14 | "**/*.tsx", 15 | "**/*.jsx", 16 | "./src/context.js", 17 | "eslint.config.mjs" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/env.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const envSchema = z 4 | .object({ 5 | PORT: z.coerce.number().default(3000), 6 | MONGO_URI: z.string(), 7 | }) 8 | .required(); 9 | 10 | const { error, data: env } = envSchema.safeParse(process.env); 11 | if (error) { 12 | console.error(error); 13 | throw new Error('failed to validate'); 14 | } 15 | 16 | export default env as z.infer; 17 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress'); 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | baseUrl: process.env.CYPRESS_BASE_URL || 'http://localhost:3000', 6 | chromeWebSecurity: false, // Required for OIDC testing 7 | setupNodeEvents(on, config) { 8 | on('task', { 9 | log(message) { 10 | console.log(message); 11 | return null; 12 | }, 13 | }); 14 | }, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/test/setupFile.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, afterAll } from '@jest/globals'; 2 | import mongoose from 'mongoose'; 3 | 4 | beforeAll(async () => { 5 | if (typeof process.env.MONGO_URI === 'string' && process.env.MONGO_URI.startsWith('mongodb')) { 6 | await mongoose.connect(process.env.MONGO_URI); 7 | console.log('done connecting mongoose'); 8 | } 9 | }); 10 | 11 | afterAll(async () => { 12 | await mongoose.disconnect(); 13 | }); 14 | -------------------------------------------------------------------------------- /src/ui/views/RepoList/RepoList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GridItem from '../../components/Grid/GridItem'; 3 | import GridContainer from '../../components/Grid/GridContainer'; 4 | import TabList from './Components/TabList'; 5 | 6 | export default function RepoList(): React.ReactElement { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | export default ({ mode }: { mode: string }) => { 5 | const env = loadEnv(mode, process.cwd(), ''); 6 | return defineConfig({ 7 | build: { 8 | outDir: 'build', 9 | }, 10 | server: { 11 | port: 3000, 12 | }, 13 | plugins: [react()], 14 | define: { 15 | 'process.env': JSON.stringify(env), 16 | }, 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /src/ui/views/UserList/UserList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GridItem from '../../components/Grid/GridItem'; 3 | import GridContainer from '../../components/Grid/GridContainer'; 4 | import TabList from './Components/TabList'; 5 | 6 | const UserList: React.FC = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default UserList; 17 | -------------------------------------------------------------------------------- /experimental/li-cli/jest.config.ts: -------------------------------------------------------------------------------- 1 | import { pathsToModuleNameMapper } from 'ts-jest'; 2 | import { compilerOptions } from './tsconfig.json'; 3 | import type { JestConfigWithTsJest } from 'ts-jest'; 4 | 5 | const jestConfig: JestConfigWithTsJest = { 6 | roots: ['/src'], 7 | modulePaths: [compilerOptions.baseUrl], 8 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths), 9 | preset: 'ts-jest', 10 | testEnvironment: 'node', 11 | }; 12 | 13 | export default jestConfig; 14 | -------------------------------------------------------------------------------- /src/ui/assets/jss/material-dashboard-react/components/cardBodyStyle.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from '@material-ui/core/styles'; 2 | 3 | const cardBodyStyle = createStyles({ 4 | cardBody: { 5 | padding: '0.9375rem 20px', 6 | flex: '1 1 auto', 7 | WebkitBoxFlex: 1, 8 | position: 'relative' as const, 9 | }, 10 | cardBodyPlain: { 11 | paddingLeft: '5px', 12 | paddingRight: '5px', 13 | }, 14 | cardBodyProfile: { 15 | marginTop: '15px', 16 | }, 17 | }); 18 | 19 | export default cardBodyStyle; 20 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/types.ts: -------------------------------------------------------------------------------- 1 | export type Result = 2 | | { 3 | error: Error; 4 | data: null; 5 | } 6 | | { 7 | error: null; 8 | data: T; 9 | }; 10 | export type AsyncResult = Promise>; 11 | 12 | export const resultIsFailure = ( 13 | r: Result, 14 | ): r is { 15 | error: Error; 16 | data: null; 17 | } => r.error !== null; 18 | export const resultIsSuccess = ( 19 | r: Result, 20 | ): r is { 21 | error: null; 22 | data: T; 23 | } => r.error === null; 24 | -------------------------------------------------------------------------------- /src/ui/views/UserList/Components/TabList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GridItem from '../../../components/Grid/GridItem'; 3 | import GridContainer from '../../../components/Grid/GridContainer'; 4 | import UserList from './UserList'; 5 | 6 | const Dashboard: React.FC = () => { 7 | return ( 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | ); 16 | }; 17 | 18 | export default Dashboard; 19 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/db/connect.ts: -------------------------------------------------------------------------------- 1 | import { AsyncResult } from '@/types'; 2 | import mongoose, { type Mongoose } from 'mongoose'; 3 | 4 | export const connectDB = async (dbURI: string): AsyncResult => { 5 | try { 6 | const connection = await mongoose.connect(dbURI); 7 | return { error: null, data: connection }; 8 | } catch (e: unknown) { 9 | if (e instanceof Error) { 10 | return { error: e, data: null }; 11 | } 12 | return { error: new Error('unknown error occured'), data: null }; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/ui/views/RepoList/Components/TabList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GridItem from '../../../components/Grid/GridItem'; 3 | import GridContainer from '../../../components/Grid/GridContainer'; 4 | import Repositories from './Repositories'; 5 | 6 | export default function Dashboard(): React.ReactElement { 7 | return ( 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/test-package/multiple-export.js: -------------------------------------------------------------------------------- 1 | const { PushActionPlugin, PullActionPlugin } = require('@finos/git-proxy/plugin'); 2 | 3 | module.exports = { 4 | foo: new PushActionPlugin(async (req, action) => { 5 | console.log('PushActionPlugin: ', action); 6 | return action; 7 | }), 8 | bar: new PullActionPlugin(async (req, action) => { 9 | console.log('PullActionPlugin: ', action); 10 | return action; 11 | }), 12 | baz: { 13 | exec: async (req, action) => { 14 | console.log('not a real plugin object'); 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/ui/views/PushDetails/components/Diff.tsx: -------------------------------------------------------------------------------- 1 | import * as Diff2Html from 'diff2html'; 2 | import reactHtmlParser from 'react-html-parser'; // Renamed to follow function naming conventions 3 | import React from 'react'; 4 | 5 | interface DiffProps { 6 | diff: string; 7 | } 8 | 9 | const Diff: React.FC = ({ diff }) => { 10 | const outputHtml = Diff2Html.html(diff, { 11 | drawFileList: true, 12 | matching: 'lines', 13 | outputFormat: 'side-by-side', 14 | }); 15 | 16 | return <>{reactHtmlParser(outputHtml)}; 17 | }; 18 | 19 | export default Diff; 20 | -------------------------------------------------------------------------------- /website/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | --- 4 | 5 | ### What is GitProxy? 6 | 7 | GitProxy deploys custom push protections and policies on top of Git. It is a highly configurable framework allowing developers and organizations to enforce push protections relevant to their developer workflow, security posture and risk appetite. 8 | 9 | GitProxy is built with a developer-first mindset. By presenting simple-to-follow remediation instructions in the CLI/Terminal, it minimises the friction of use and adoption, and keeps developers focused on what matters; committing and pushing code. -------------------------------------------------------------------------------- /src/ui/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { PublicUser } from '../db/types'; 3 | 4 | export const UserContext = createContext({ 5 | user: { 6 | admin: false, 7 | }, 8 | }); 9 | 10 | export interface UserContextType { 11 | user: { 12 | admin: boolean; 13 | }; 14 | } 15 | 16 | export interface AuthContextType { 17 | user: PublicUser | null; 18 | setUser: React.Dispatch; 19 | refreshUser: () => Promise; 20 | isLoading: boolean; 21 | } 22 | 23 | export const AuthContext = createContext(undefined); 24 | -------------------------------------------------------------------------------- /src/config/env.ts: -------------------------------------------------------------------------------- 1 | import { ServerConfig } from './types'; 2 | 3 | const { 4 | GIT_PROXY_SERVER_PORT = 8000, 5 | GIT_PROXY_HTTPS_SERVER_PORT = 8443, 6 | GIT_PROXY_UI_HOST = 'http://localhost', 7 | GIT_PROXY_UI_PORT = 8080, 8 | GIT_PROXY_COOKIE_SECRET, 9 | GIT_PROXY_MONGO_CONNECTION_STRING = 'mongodb://localhost:27017/git-proxy', 10 | } = process.env; 11 | 12 | export const serverConfig: ServerConfig = { 13 | GIT_PROXY_SERVER_PORT, 14 | GIT_PROXY_HTTPS_SERVER_PORT, 15 | GIT_PROXY_UI_HOST, 16 | GIT_PROXY_UI_PORT, 17 | GIT_PROXY_COOKIE_SECRET, 18 | GIT_PROXY_MONGO_CONNECTION_STRING, 19 | }; 20 | -------------------------------------------------------------------------------- /test/fixtures/test-package/esm-multiple-export.js: -------------------------------------------------------------------------------- 1 | import { PushActionPlugin, PullActionPlugin } from '@finos/git-proxy/plugin'; 2 | 3 | // test multiple exports (ESM syntax) 4 | export default { 5 | foo: new PushActionPlugin(async (req, action) => { 6 | console.log('PushActionPlugin: ', action); 7 | return action; 8 | }), 9 | bar: new PullActionPlugin(async (req, action) => { 10 | console.log('PullActionPlugin: ', action); 11 | return action; 12 | }), 13 | baz: { 14 | exec: async (req, action) => { 15 | console.log('not a real plugin object'); 16 | }, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /experimental/license-inventory/dev/prometheus.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | evaluation_interval: 15s 4 | 5 | scrape_configs: 6 | - job_name: prometheus 7 | static_configs: 8 | - targets: ['localhost:9090'] 9 | - job_name: tempo 10 | static_configs: 11 | - targets: ['tempo:3200'] 12 | - job_name: promtail 13 | static_configs: 14 | - targets: ['promtail:9080'] 15 | - job_name: otel-collector 16 | static_configs: 17 | - targets: ['otel-collector:8889'] 18 | - job_name: otel-collector-meta 19 | static_configs: 20 | - targets: ['otel-collector:8888'] 21 | -------------------------------------------------------------------------------- /src/ui/components/Typography/Info.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import clsx from 'clsx'; 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | import styles from '../../assets/jss/material-dashboard-react/components/typographyStyle'; 6 | 7 | const useStyles = makeStyles(styles); 8 | 9 | export default function Info(props) { 10 | const classes = useStyles(); 11 | const { children } = props; 12 | return
{children}
; 13 | } 14 | 15 | Info.propTypes = { 16 | children: PropTypes.node, 17 | }; 18 | -------------------------------------------------------------------------------- /src/ui/components/Typography/Danger.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import clsx from 'clsx'; 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | import styles from '../../assets/jss/material-dashboard-react/components/typographyStyle'; 6 | 7 | const useStyles = makeStyles(styles); 8 | 9 | export default function Danger(props) { 10 | const classes = useStyles(); 11 | const { children } = props; 12 | return
{children}
; 13 | } 14 | 15 | Danger.propTypes = { 16 | children: PropTypes.node, 17 | }; 18 | -------------------------------------------------------------------------------- /plugins/git-proxy-plugin-samples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@finos/git-proxy-plugin-samples", 3 | "version": "0.1.1", 4 | "description": "A set of sample (dummy) plugins for GitProxy to demonstrate how plugins are authored.", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "Thomas Cooper", 9 | "license": "Apache-2.0", 10 | "type": "module", 11 | "exports": { 12 | ".": "./index.js", 13 | "./example": "./example.cjs" 14 | }, 15 | "dependencies": { 16 | "express": "^5.1.0" 17 | }, 18 | "peerDependencies": { 19 | "@finos/git-proxy": "^1.19.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/proxy/processors/push-action/clearBareClone.ts: -------------------------------------------------------------------------------- 1 | import { Action, Step } from '../../actions'; 2 | import fs from 'node:fs'; 3 | 4 | const exec = async (req: any, action: Action): Promise => { 5 | const step = new Step('clearBareClone'); 6 | 7 | // Recursively remove the contents of ./.remote and ignore exceptions 8 | fs.rm('./.remote', { recursive: true, force: true }, (err) => { 9 | if (err) { 10 | throw err; 11 | } 12 | console.log(`.remote is deleted!`); 13 | }); 14 | 15 | action.addStep(step); 16 | return action; 17 | }; 18 | 19 | exec.displayName = 'clearBareClone.exec'; 20 | 21 | export { exec }; 22 | -------------------------------------------------------------------------------- /src/ui/components/Typography/Muted.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import clsx from 'clsx'; 4 | // @material-ui/core components 5 | import { makeStyles } from '@material-ui/core/styles'; 6 | // core components 7 | import styles from '../../assets/jss/material-dashboard-react/components/typographyStyle'; 8 | 9 | const useStyles = makeStyles(styles); 10 | 11 | export default function Muted(props) { 12 | const classes = useStyles(); 13 | const { children } = props; 14 | return
{children}
; 15 | } 16 | 17 | Muted.propTypes = { 18 | children: PropTypes.node, 19 | }; 20 | -------------------------------------------------------------------------------- /experimental/license-inventory/jest.config.ts: -------------------------------------------------------------------------------- 1 | import { pathsToModuleNameMapper } from 'ts-jest'; 2 | import { compilerOptions } from './tsconfig.json'; 3 | import type { JestConfigWithTsJest } from 'ts-jest'; 4 | 5 | const jestConfig: JestConfigWithTsJest = { 6 | roots: ['/src'], 7 | modulePaths: [compilerOptions.baseUrl], 8 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths), 9 | preset: 'ts-jest', 10 | testEnvironment: 'node', 11 | globalSetup: '/src/test/globalSetup.ts', 12 | globalTeardown: '/src/test/globalTeardown.ts', 13 | setupFilesAfterEnv: ['/src/test/setupFile.ts'], 14 | }; 15 | 16 | export default jestConfig; 17 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/services/data/index.ts: -------------------------------------------------------------------------------- 1 | import type { AsyncResult } from '@/types'; 2 | import type { License, LicenseNoID, LicenseNoIDPartial } from './license'; 3 | 4 | export interface LicenseDataService { 5 | create: (licenseData: LicenseNoID) => AsyncResult; 6 | 7 | getByUUID: (id: string) => AsyncResult; 8 | 9 | patchByUUID: (id: string, licenseData: LicenseNoIDPartial) => AsyncResult; 10 | 11 | deleteByUUID: (id: string) => AsyncResult; 12 | 13 | // TODO: consider pagination 14 | list: () => AsyncResult; 15 | } 16 | 17 | export interface DataService { 18 | licenses: LicenseDataService; 19 | } 20 | -------------------------------------------------------------------------------- /src/ui/components/Typography/Warning.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import clsx from 'clsx'; 4 | // @material-ui/core components 5 | import { makeStyles } from '@material-ui/core/styles'; 6 | // core components 7 | import styles from 'ui/assets/jss/material-dashboard-react/components/typographyStyle'; 8 | 9 | const useStyles = makeStyles(styles); 10 | 11 | export default function Warning(props) { 12 | const classes = useStyles(); 13 | const { children } = props; 14 | return
{children}
; 15 | } 16 | 17 | Warning.propTypes = { 18 | children: PropTypes.node, 19 | }; 20 | -------------------------------------------------------------------------------- /src/ui/components/Typography/Primary.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import clsx from 'clsx'; 4 | // @material-ui/core components 5 | import { makeStyles } from '@material-ui/core/styles'; 6 | // core components 7 | import styles from '../../assets/jss/material-dashboard-react/components/typographyStyle'; 8 | 9 | const useStyles = makeStyles(styles); 10 | 11 | export default function Primary(props) { 12 | const classes = useStyles(); 13 | const { children } = props; 14 | return
{children}
; 15 | } 16 | 17 | Primary.propTypes = { 18 | children: PropTypes.node, 19 | }; 20 | -------------------------------------------------------------------------------- /src/ui/components/Typography/Success.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import clsx from 'clsx'; 4 | // @material-ui/core components 5 | import { makeStyles } from '@material-ui/core/styles'; 6 | // core components 7 | import styles from '../../assets/jss/material-dashboard-react/components/typographyStyle'; 8 | 9 | const useStyles = makeStyles(styles); 10 | 11 | export default function Success(props) { 12 | const classes = useStyles(); 13 | const { children } = props; 14 | return
{children}
; 15 | } 16 | 17 | Success.propTypes = { 18 | children: PropTypes.node, 19 | }; 20 | -------------------------------------------------------------------------------- /src/proxy/processors/push-action/blockForAuth.ts: -------------------------------------------------------------------------------- 1 | import { Action, Step } from '../../actions'; 2 | import { getServiceUIURL } from '../../../service/urls'; 3 | 4 | const exec = async (req: any, action: Action) => { 5 | const step = new Step('authBlock'); 6 | const url = getServiceUIURL(req); 7 | 8 | const message = 9 | '\n\n\n' + 10 | `\x1B[32mGitProxy has received your push ✅\x1B[0m\n\n` + 11 | '🔗 Shareable Link\n\n' + 12 | `\x1B[34m${url}/dashboard/push/${action.id}\x1B[0m` + 13 | '\n\n\n'; 14 | step.setAsyncBlock(message); 15 | 16 | action.addStep(step); 17 | return action; 18 | }; 19 | 20 | exec.displayName = 'blockForAuth.exec'; 21 | 22 | export { exec }; 23 | -------------------------------------------------------------------------------- /src/service/routes/config.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | import * as config from '../../config'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/attestation', (_req: Request, res: Response) => { 7 | res.send(config.getAttestationConfig()); 8 | }); 9 | 10 | router.get('/urlShortener', (_req: Request, res: Response) => { 11 | res.send(config.getURLShortener()); 12 | }); 13 | 14 | router.get('/contactEmail', (_req: Request, res: Response) => { 15 | res.send(config.getContactEmail()); 16 | }); 17 | 18 | router.get('/uiRouteAuth', (_req: Request, res: Response) => { 19 | res.send(config.getUIRouteAuth()); 20 | }); 21 | 22 | export default router; 23 | -------------------------------------------------------------------------------- /src/service/routes/utils.ts: -------------------------------------------------------------------------------- 1 | import { PublicUser, User as DbUser } from '../../db/types'; 2 | 3 | interface User extends Express.User { 4 | username: string; 5 | admin?: boolean; 6 | } 7 | 8 | export function isAdminUser(user?: Express.User): user is User & { admin: true } { 9 | return user !== null && user !== undefined && (user as User).admin === true; 10 | } 11 | 12 | export const toPublicUser = (user: DbUser): PublicUser => { 13 | return { 14 | username: user.username || '', 15 | displayName: user.displayName || '', 16 | email: user.email || '', 17 | title: user.title || '', 18 | gitAccount: user.gitAccount || '', 19 | admin: user.admin || false, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/test/globalTeardown.ts: -------------------------------------------------------------------------------- 1 | import { MongoMemoryServer } from 'mongodb-memory-server-core'; 2 | import { config } from './utils/config'; 3 | 4 | declare global { 5 | // eslint-disable-next-line no-var 6 | var __MONGOINSTANCE: MongoMemoryServer | undefined; 7 | } 8 | 9 | export default async function globalTeardown() { 10 | if (config.Memory) { 11 | // Config to decide if an mongodb-memory-server instance should be used 12 | if (!(global.__MONGOINSTANCE instanceof MongoMemoryServer)) { 13 | throw new Error('expect MongoMemoryServer'); 14 | } 15 | const instance: MongoMemoryServer = global.__MONGOINSTANCE; 16 | await instance.stop(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ui/components/Grid/GridItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { makeStyles, Theme } from '@material-ui/core/styles'; 3 | import Grid, { GridProps } from '@material-ui/core/Grid'; 4 | 5 | const useStyles = makeStyles((theme: Theme) => ({ 6 | grid: { 7 | padding: '0 15px !important', 8 | }, 9 | })); 10 | 11 | export interface GridItemProps extends GridProps { 12 | children?: ReactNode; 13 | } 14 | 15 | const GridItem: React.FC = ({ children, ...rest }) => { 16 | const classes = useStyles(); 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | }; 23 | 24 | export default GridItem; 25 | -------------------------------------------------------------------------------- /src/service/urls.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | import { serverConfig } from '../config/env'; 4 | import * as config from '../config'; 5 | 6 | const { GIT_PROXY_SERVER_PORT: PROXY_HTTP_PORT, GIT_PROXY_UI_PORT: UI_PORT } = serverConfig; 7 | 8 | export const getProxyURL = (req: Request): string => { 9 | return ( 10 | config.getDomains().proxy ?? 11 | `${req.protocol}://${req.headers.host}`.replace(`:${UI_PORT}`, `:${PROXY_HTTP_PORT}`) 12 | ); 13 | }; 14 | 15 | export const getServiceUIURL = (req: Request): string => { 16 | return ( 17 | config.getDomains().service ?? 18 | `${req.protocol}://${req.headers.host}`.replace(`:${PROXY_HTTP_PORT}`, `:${UI_PORT}`) 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/ui/assets/jss/material-dashboard-react/cardImagesStyles.js: -------------------------------------------------------------------------------- 1 | const cardImagesStyles = { 2 | cardImgTop: { 3 | width: '100%', 4 | borderTopLeftRadius: 'calc(.25rem - 1px)', 5 | borderTopRightRadius: 'calc(.25rem - 1px)', 6 | }, 7 | cardImgBottom: { 8 | width: '100%', 9 | borderBottomRightRadius: 'calc(.25rem - 1px)', 10 | borderBottomLeftRadius: 'calc(.25rem - 1px)', 11 | }, 12 | cardImgOverlay: { 13 | position: 'absolute', 14 | top: '0', 15 | right: '0', 16 | bottom: '0', 17 | left: '0', 18 | padding: '1.25rem', 19 | }, 20 | cardImg: { 21 | width: '100%', 22 | borderRadius: 'calc(.25rem - 1px)', 23 | }, 24 | }; 25 | 26 | export default cardImagesStyles; 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "lib": ["DOM", "ESNext"], 5 | "allowJs": true, 6 | "checkJs": false, 7 | "jsx": "react-jsx", 8 | "moduleResolution": "Node", 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "isolatedModules": false, 12 | "module": "CommonJS", 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | "resolveJsonModule": true, 16 | 17 | "declaration": true, 18 | "declarationMap": true, 19 | "outDir": "./dist", 20 | "rootDir": "./src", 21 | "noEmit": false, 22 | "types": ["node"] 23 | }, 24 | "include": ["src"], 25 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /test/fixtures/gitleaks-config.toml: -------------------------------------------------------------------------------- 1 | title = "sample gitleaks config" 2 | 3 | [[rules]] 4 | id = "generic-api-key" 5 | description = "Generic API Key" 6 | regex = '''(?i)(?:key|api|token|secret)[\s:=]+([a-z0-9]{32,})''' 7 | tags = ["key", "api-key"] 8 | 9 | [[rules]] 10 | id = "aws-access-key-id" 11 | description = "AWS Access Key ID" 12 | regex = '''AKIA[0-9A-Z]{16}''' 13 | tags = ["aws", "key"] 14 | 15 | [[rules]] 16 | id = "basic-auth" 17 | description = "Auth Credentials" 18 | regex = '''(?i)(https?://)[a-z0-9]+:[a-z0-9]+@''' 19 | tags = ["auth", "password"] 20 | 21 | [[rules]] 22 | id = "jwt-token" 23 | description = "JSON Web Token" 24 | regex = '''eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.?[A-Za-z0-9._-]*''' 25 | tags = ["jwt", "token"] 26 | -------------------------------------------------------------------------------- /scripts/add-banner.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | const banner = '// THIS FILE IS AUTOMATICALLY GENERATED – DO NOT EDIT MANUALLY.\n\n'; 5 | 6 | const filePath = process.argv[2]; 7 | 8 | if (!filePath) { 9 | console.error('Error: Provide a file path as an argument.'); 10 | process.exit(1); 11 | } 12 | 13 | const resolvedPath = path.resolve(filePath); 14 | 15 | if (!fs.existsSync(resolvedPath)) { 16 | console.error(`Error: The file "${resolvedPath}" does not exist.`); 17 | process.exit(1); 18 | } 19 | 20 | const originalContent = fs.readFileSync(resolvedPath, 'utf8'); 21 | 22 | if (!originalContent.startsWith(banner)) { 23 | fs.writeFileSync(resolvedPath, banner + originalContent, 'utf8'); 24 | } 25 | -------------------------------------------------------------------------------- /src/db/file/index.ts: -------------------------------------------------------------------------------- 1 | import * as users from './users'; 2 | import * as repo from './repo'; 3 | import * as pushes from './pushes'; 4 | import * as helper from './helper'; 5 | 6 | export const { getSessionStore } = helper; 7 | 8 | export const { getPushes, writeAudit, getPush, deletePush, authorise, cancel, reject } = pushes; 9 | 10 | export const { 11 | getRepos, 12 | getRepo, 13 | getRepoByUrl, 14 | getRepoById, 15 | createRepo, 16 | addUserCanPush, 17 | addUserCanAuthorise, 18 | removeUserCanPush, 19 | removeUserCanAuthorise, 20 | deleteRepo, 21 | } = repo; 22 | 23 | export const { 24 | findUser, 25 | findUserByEmail, 26 | findUserByOIDC, 27 | getUsers, 28 | createUser, 29 | deleteUser, 30 | updateUser, 31 | } = users; 32 | -------------------------------------------------------------------------------- /src/db/mongo/index.ts: -------------------------------------------------------------------------------- 1 | import * as helper from './helper'; 2 | import * as pushes from './pushes'; 3 | import * as repo from './repo'; 4 | import * as users from './users'; 5 | 6 | export const { getSessionStore } = helper; 7 | 8 | export const { getPushes, writeAudit, getPush, deletePush, authorise, cancel, reject } = pushes; 9 | 10 | export const { 11 | getRepos, 12 | getRepo, 13 | getRepoByUrl, 14 | getRepoById, 15 | createRepo, 16 | addUserCanPush, 17 | addUserCanAuthorise, 18 | removeUserCanPush, 19 | removeUserCanAuthorise, 20 | deleteRepo, 21 | } = repo; 22 | 23 | export const { 24 | findUser, 25 | findUserByEmail, 26 | findUserByOIDC, 27 | getUsers, 28 | createUser, 29 | deleteUser, 30 | updateUser, 31 | } = users; 32 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /packages/git-proxy-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@finos/git-proxy-cli", 3 | "version": "2.0.0-rc.3", 4 | "description": "Command line interface tool for FINOS GitProxy.", 5 | "bin": { 6 | "git-proxy-cli": "./dist/index.js" 7 | }, 8 | "dependencies": { 9 | "axios": "^1.13.2", 10 | "yargs": "^17.7.2", 11 | "@finos/git-proxy": "2.0.0-rc.3" 12 | }, 13 | "scripts": { 14 | "build": "tsc", 15 | "lint": "eslint \"./*.ts\" --fix", 16 | "test": "cd ../.. && vitest --run --dir packages/git-proxy-cli/test" 17 | }, 18 | "author": "Miklos Sagi", 19 | "license": "Apache-2.0", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/finos/git-proxy", 23 | "path": "packages/git-proxy-cli" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/db/helper.ts: -------------------------------------------------------------------------------- 1 | export const toClass = function (obj: T, proto: U): U { 2 | const out = JSON.parse(JSON.stringify(obj)); 3 | out.__proto__ = proto; 4 | return out as U; 5 | }; 6 | 7 | export const trimTrailingDotGit = (str: string): string => { 8 | const target = '.git'; 9 | if (str && str.endsWith(target)) { 10 | // extract string from 0 to the end minus the length of target 11 | return str.slice(0, -target.length); 12 | } 13 | return str; 14 | }; 15 | 16 | export const trimPrefixRefsHeads = (str: string): string => { 17 | const target = 'refs/heads/'; 18 | if (str.startsWith(target)) { 19 | // extract string from the end of the target to the end of str 20 | return str.slice(target.length); 21 | } 22 | return str; 23 | }; 24 | -------------------------------------------------------------------------------- /experimental/license-inventory/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import pluginJs from '@eslint/js'; 3 | import tseslint from 'typescript-eslint'; 4 | import { includeIgnoreFile } from '@eslint/compat'; 5 | 6 | import path from 'node:path'; 7 | import { fileURLToPath } from 'node:url'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const gitignorePath = path.resolve(__dirname, '.gitignore'); 12 | 13 | /** @type {import('eslint').Linter.Config[]} */ 14 | export default [ 15 | includeIgnoreFile(gitignorePath), 16 | { files: ['**/*.{js,mjs,cjs,ts}'] }, 17 | { languageOptions: { globals: globals.node } }, 18 | pluginJs.configs.recommended, 19 | ...tseslint.configs.recommended, 20 | ]; 21 | -------------------------------------------------------------------------------- /src/ui/components/Grid/GridContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import Grid from '@material-ui/core/Grid'; 4 | import { GridProps } from '@material-ui/core/Grid'; 5 | 6 | const styles = { 7 | grid: { 8 | margin: '0 -15px !important', 9 | width: 'unset', 10 | }, 11 | }; 12 | 13 | const useStyles = makeStyles(styles); 14 | 15 | interface GridContainerProps extends GridProps { 16 | children?: React.ReactNode; 17 | } 18 | 19 | export default function GridContainer(props: GridContainerProps) { 20 | const classes = useStyles(); 21 | const { children, ...rest } = props; 22 | return ( 23 | 24 | {children} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:best-practices", 5 | ":separateMultipleMajorReleases", 6 | "group:allNonMajor", 7 | "group:allDigest", 8 | "group:jsTest", 9 | "group:linters", 10 | "group:nodeJs" 11 | ], 12 | "additionalBranchPrefix": "{{parentDir}}-", 13 | "commitMessageSuffix": "- {{parentDir}} - {{packageFile}}", 14 | "packageRules": [ 15 | { 16 | "matchDatasources": ["npm"], 17 | "rangeStrategy": "bump" 18 | }, 19 | { 20 | "matchPackageNames": ["*"], 21 | "groupName": "{{manager}}" 22 | }, 23 | { 24 | "semanticCommitScope": "experimental", 25 | "matchFileNames": ["experimental/*"] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /packages/git-proxy-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "lib": ["DOM", "ESNext"], 5 | "allowJs": true, 6 | "checkJs": false, 7 | "jsx": "react-jsx", 8 | "moduleResolution": "nodenext", 9 | "strict": true, 10 | "declaration": true, 11 | "skipLibCheck": true, 12 | "isolatedModules": true, 13 | "module": "NodeNext", 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true, 16 | "resolveJsonModule": true, 17 | "outDir": "./dist", 18 | "rootDir": "." 19 | }, 20 | "include": ["index.ts", "types.ts"], 21 | "exclude": [ 22 | "src/config/**/*", 23 | "src/db/**/*", 24 | "src/proxy/**/*", 25 | "src/service/**/*", 26 | "src/ui/**/*", 27 | "eslint.config.mjs" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /packages/git-proxy-cli/test/testCli.proxy.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "tempPassword": { 3 | "sendEmail": false, 4 | "emailConfig": {} 5 | }, 6 | "authorisedList": [ 7 | { 8 | "project": "msagi", 9 | "name": "git-proxy-test", 10 | "url": "https://github.com/msagi/git-proxy-test.git" 11 | } 12 | ], 13 | "sink": [ 14 | { 15 | "type": "fs", 16 | "params": { 17 | "filepath": "./." 18 | }, 19 | "enabled": true 20 | }, 21 | { 22 | "type": "mongo", 23 | "connectionString": "mongodb://localhost:27017/gitproxy", 24 | "options": { 25 | "useUnifiedTopology": true 26 | }, 27 | "enabled": false 28 | } 29 | ], 30 | "authentication": [ 31 | { 32 | "type": "local", 33 | "enabled": true 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/proxy/processors/push-action/checkIfWaitingAuth.ts: -------------------------------------------------------------------------------- 1 | import { Action, Step } from '../../actions'; 2 | import { getPush } from '../../../db'; 3 | 4 | // Execute function 5 | const exec = async (req: any, action: Action): Promise => { 6 | const step = new Step('checkIfWaitingAuth'); 7 | try { 8 | const existingAction = await getPush(action.id); 9 | if (existingAction) { 10 | if (!action.error) { 11 | if (existingAction.authorised) { 12 | action = existingAction; 13 | action.setAllowPush(); 14 | } 15 | } 16 | } 17 | } catch (e: any) { 18 | step.setError(e.toString('utf-8')); 19 | throw e; 20 | } finally { 21 | action.addStep(step); 22 | } 23 | return action; 24 | }; 25 | 26 | exec.displayName = 'checkIfWaitingAuth.exec'; 27 | 28 | export { exec }; 29 | -------------------------------------------------------------------------------- /src/ui/components/Pagination/Pagination.css: -------------------------------------------------------------------------------- 1 | .paginationContainer { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | padding: 1rem; 6 | margin-top: 20px; 7 | gap: 10px; 8 | } 9 | 10 | .pageButton { 11 | padding: 8px 12px; 12 | font-size: 14px; 13 | color: #333; 14 | border: 1px solid #ccc; 15 | background-color: #f9f9f9; 16 | cursor: pointer; 17 | border-radius: 5px; 18 | transition: background-color 0.3s ease; 19 | } 20 | 21 | .pageButton:hover:not(:disabled) { 22 | background-color: #e2e6ea; 23 | } 24 | 25 | .pageButton:disabled { 26 | background-color: #e9ecef; 27 | color: #6c757d; 28 | border-color: #dee2e6; 29 | cursor: not-allowed; 30 | opacity: 0.6; 31 | } 32 | 33 | .activeButton { 34 | background-color: #007bff; 35 | color: #fff; 36 | border-color: #007bff; 37 | } 38 | -------------------------------------------------------------------------------- /src/proxy/processors/push-action/checkRepoInAuthorisedList.ts: -------------------------------------------------------------------------------- 1 | import { Action, Step } from '../../actions'; 2 | import { getRepoByUrl } from '../../../db'; 3 | 4 | // Execute if the repo is approved 5 | const exec = async (req: any, action: Action): Promise => { 6 | const step = new Step('checkRepoInAuthorisedList'); 7 | 8 | const found = (await getRepoByUrl(action.url)) !== null; 9 | if (found) { 10 | step.log(`repo ${action.url} is in the authorisedList`); 11 | } else { 12 | step.error = true; 13 | step.log(`repo ${action.url} is not in the authorised whitelist, ending`); 14 | step.setError(`Rejecting repo ${action.url} not in the authorised whitelist`); 15 | } 16 | 17 | action.addStep(step); 18 | return action; 19 | }; 20 | 21 | exec.displayName = 'checkRepoInAuthorisedList.exec'; 22 | 23 | export { exec }; 24 | -------------------------------------------------------------------------------- /src/service/routes/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import auth from './auth'; 3 | import push from './push'; 4 | import home from './home'; 5 | import repo from './repo'; 6 | import users from './users'; 7 | import healthcheck from './healthcheck'; 8 | import config from './config'; 9 | import { jwtAuthHandler } from '../passport/jwtAuthHandler'; 10 | 11 | const routes = (proxy: any) => { 12 | const router = express.Router(); 13 | router.use('/api', home); 14 | router.use('/api/auth', auth.router); 15 | router.use('/api/v1/healthcheck', healthcheck); 16 | router.use('/api/v1/push', jwtAuthHandler(), push); 17 | router.use('/api/v1/repo', jwtAuthHandler(), repo(proxy)); 18 | router.use('/api/v1/user', jwtAuthHandler(), users); 19 | router.use('/api/v1/config', config); 20 | return router; 21 | }; 22 | 23 | export default routes; 24 | -------------------------------------------------------------------------------- /src/ui/components/Typography/Quote.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import clsx from 'clsx'; 4 | // @material-ui/core components 5 | import { makeStyles } from '@material-ui/core/styles'; 6 | // core components 7 | import styles from '../../assets/jss/material-dashboard-react/components/typographyStyle'; 8 | 9 | const useStyles = makeStyles(styles); 10 | 11 | export default function Quote(props) { 12 | const classes = useStyles(); 13 | const { text, author } = props; 14 | return ( 15 |
16 |

{text}

17 | {author} 18 |
19 | ); 20 | } 21 | 22 | Quote.propTypes = { 23 | text: PropTypes.node, 24 | author: PropTypes.node, 25 | }; 26 | -------------------------------------------------------------------------------- /src/ui/assets/jss/material-dashboard-react/layouts/rtlStyle.js: -------------------------------------------------------------------------------- 1 | import { drawerWidth, transition, container } from '../../jss/material-dashboard-react.js'; 2 | 3 | const appStyle = (theme) => ({ 4 | wrapper: { 5 | position: 'relative', 6 | top: '0', 7 | height: '100vh', 8 | direction: 'rtl', 9 | }, 10 | mainPanel: { 11 | [theme.breakpoints.up('md')]: { 12 | width: `calc(100% - ${drawerWidth}px)`, 13 | }, 14 | overflow: 'auto', 15 | position: 'relative', 16 | float: 'left', 17 | ...transition, 18 | maxHeight: '100%', 19 | width: '100%', 20 | overflowScrolling: 'touch', 21 | }, 22 | content: { 23 | marginTop: '70px', 24 | padding: '30px 15px', 25 | minHeight: 'calc(100vh - 123px)', 26 | }, 27 | container, 28 | map: { 29 | marginTop: '70px', 30 | }, 31 | }); 32 | 33 | export default appStyle; 34 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | pool: 'forks', 6 | poolOptions: { 7 | forks: { 8 | singleFork: true, // Run all tests in a single process 9 | }, 10 | }, 11 | coverage: { 12 | provider: 'v8', 13 | reportsDirectory: './coverage', 14 | reporter: ['text', 'lcov'], 15 | include: ['src/**/*.ts'], 16 | exclude: [ 17 | 'dist', 18 | 'experimental', 19 | 'packages', 20 | 'plugins', 21 | 'scripts', 22 | 'src/**/types.ts', 23 | 'src/config/generated', 24 | 'src/constants', 25 | 'src/contents', 26 | 'src/types', 27 | 'src/ui', 28 | 'website', 29 | ], 30 | thresholds: { 31 | lines: 80, 32 | }, 33 | }, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/service/routes/users.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | const router = express.Router(); 3 | 4 | import * as db from '../../db'; 5 | import { toPublicUser } from './utils'; 6 | 7 | router.get('/', async (req: Request, res: Response) => { 8 | console.log('fetching users'); 9 | const users = await db.getUsers(); 10 | res.send(users.map(toPublicUser)); 11 | }); 12 | 13 | router.get('/:id', async (req: Request, res: Response) => { 14 | const username = req.params.id.toLowerCase(); 15 | console.log(`Retrieving details for user: ${username}`); 16 | const user = await db.findUser(username); 17 | if (!user) { 18 | res 19 | .status(404) 20 | .send({ 21 | message: `User ${username} not found`, 22 | }) 23 | .end(); 24 | return; 25 | } 26 | res.send(toPublicUser(user)); 27 | }); 28 | 29 | export default router; 30 | -------------------------------------------------------------------------------- /src/ui/assets/jss/material-dashboard-react/components/cardIconStyle.js: -------------------------------------------------------------------------------- 1 | import { 2 | warningCardHeader, 3 | successCardHeader, 4 | dangerCardHeader, 5 | infoCardHeader, 6 | primaryCardHeader, 7 | roseCardHeader, 8 | grayColor, 9 | } from '../../material-dashboard-react.js'; 10 | 11 | const cardIconStyle = { 12 | cardIcon: { 13 | '&$warningCardHeader,&$successCardHeader,&$dangerCardHeader,&$infoCardHeader,&$primaryCardHeader,&$roseCardHeader': 14 | { 15 | borderRadius: '3px', 16 | backgroundColor: grayColor[0], 17 | padding: '15px', 18 | marginTop: '-20px', 19 | marginRight: '15px', 20 | float: 'left', 21 | }, 22 | }, 23 | warningCardHeader, 24 | successCardHeader, 25 | dangerCardHeader, 26 | infoCardHeader, 27 | primaryCardHeader, 28 | roseCardHeader, 29 | }; 30 | 31 | export default cardIconStyle; 32 | -------------------------------------------------------------------------------- /website/docs/usage.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Usage 3 | description: How to run GitProxy in your environment 4 | --- 5 | 6 | ### Run from global install 7 | 8 | Once you have followed the [installation](installation) steps, run: 9 | 10 | ```bash 11 | git-proxy 12 | ``` 13 | 14 | To run GitProxy using the CLI: 15 | 16 | ```bash 17 | git-proxy-cli 18 | ``` 19 | 20 | ### Using [npx instead of npm](https://www.freecodecamp.org/news/npm-vs-npx-whats-the-difference/) 21 | 22 | You can also install & run `git-proxy` and `git-proxy-cli` in two steps: 23 | 24 | ```bash 25 | npx -- @finos/git-proxy 26 | ``` 27 | 28 | ```bash 29 | npx -- @finos/git-proxy-cli 30 | ``` 31 | 32 | If you use a proxy server to access `npm` then please note that you also need to configure your proxy for `npx` before running the above commands: 33 | 34 | ```bash 35 | npx config set https-proxy 36 | ``` 37 | -------------------------------------------------------------------------------- /experimental/li-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "ES2015", 5 | "strict": true, 6 | "lib": ["ES2022", "esnext.asynciterable"], 7 | "typeRoots": ["node_modules/@types"], 8 | "allowSyntheticDefaultImports": true, 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "module": "commonjs", 14 | "pretty": true, 15 | "sourceMap": true, 16 | "declaration": true, 17 | "outDir": "dist", 18 | "allowJs": false, 19 | "noEmit": true, 20 | "esModuleInterop": true, 21 | "resolveJsonModule": true, 22 | "importHelpers": true, 23 | "baseUrl": "./src", 24 | "paths": { 25 | "@/*": ["*"] 26 | } 27 | }, 28 | "include": ["src/**/*.ts"], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/server.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@/logger'; 2 | import { connectDB } from './db/connect'; 3 | import env from '@/env'; 4 | import { createApp } from './app'; 5 | import { MongooseLicenseDataService } from './services/data/mongoose'; 6 | import { Database } from './db'; 7 | 8 | const port = env.PORT; 9 | 10 | const run = async () => { 11 | logger.info('starting server', { port }); 12 | 13 | const { error, data: dbConnection } = await connectDB(env.MONGO_URI); 14 | if (error !== null) { 15 | logger.error(error); 16 | throw new Error('failed to connect to mongo'); 17 | } 18 | const db = new Database(dbConnection); 19 | 20 | const running = () => { 21 | logger.info('started server', { port }); 22 | }; 23 | 24 | const lds = new MongooseLicenseDataService(db); 25 | const app = createApp(lds); 26 | app.listen(port, running); 27 | }; 28 | run(); 29 | -------------------------------------------------------------------------------- /src/service/passport/types.ts: -------------------------------------------------------------------------------- 1 | import { JwtPayload } from 'jsonwebtoken'; 2 | 3 | export type JwkKey = { 4 | kty: string; 5 | kid: string; 6 | use: string; 7 | n?: string; 8 | e?: string; 9 | x5c?: string[]; 10 | [key: string]: any; 11 | }; 12 | 13 | export type JwksResponse = { 14 | keys: JwkKey[]; 15 | }; 16 | 17 | export type JwtValidationResult = { 18 | verifiedPayload: JwtPayload | null; 19 | error: string | null; 20 | }; 21 | 22 | export type ADProfile = { 23 | id?: string; 24 | username?: string; 25 | email?: string; 26 | displayName?: string; 27 | admin?: boolean; 28 | _json: ADProfileJson; 29 | }; 30 | 31 | export type ADProfileJson = { 32 | sAMAccountName?: string; 33 | mail?: string; 34 | title?: string; 35 | userPrincipalName?: string; 36 | [key: string]: any; 37 | }; 38 | 39 | export type ADVerifyCallback = (err: Error | null, user: ADProfile | null) => void; 40 | -------------------------------------------------------------------------------- /src/ui/assets/jss/material-dashboard-react/components/cardAvatarStyle.js: -------------------------------------------------------------------------------- 1 | import { hexToRgb, blackColor } from '../../material-dashboard-react.js'; 2 | 3 | const cardAvatarStyle = { 4 | cardAvatar: { 5 | '&$cardAvatarProfile img': { 6 | width: '100%', 7 | height: 'auto', 8 | }, 9 | }, 10 | cardAvatarProfile: { 11 | maxWidth: '130px', 12 | maxHeight: '130px', 13 | margin: '-50px auto 0', 14 | borderRadius: '50%', 15 | overflow: 'hidden', 16 | padding: '0', 17 | boxShadow: 18 | '0 16px 38px -12px rgba(' + 19 | hexToRgb(blackColor) + 20 | ', 0.56), 0 4px 25px 0px rgba(' + 21 | hexToRgb(blackColor) + 22 | ', 0.12), 0 8px 10px -5px rgba(' + 23 | hexToRgb(blackColor) + 24 | ', 0.2)', 25 | '&$cardAvatarPlain': { 26 | marginTop: '0', 27 | }, 28 | }, 29 | cardAvatarPlain: {}, 30 | }; 31 | 32 | export default cardAvatarStyle; 33 | -------------------------------------------------------------------------------- /experimental/license-inventory/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "es2017", 5 | "strict": true, 6 | "lib": ["ES2022", "esnext.asynciterable"], 7 | "typeRoots": ["node_modules/@types"], 8 | "allowSyntheticDefaultImports": true, 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "module": "commonjs", 14 | "pretty": true, 15 | "sourceMap": true, 16 | "declaration": true, 17 | "outDir": "dist", 18 | "allowJs": false, 19 | "noEmit": true, 20 | "esModuleInterop": true, 21 | "resolveJsonModule": true, 22 | "importHelpers": true, 23 | "baseUrl": "./src", 24 | "paths": { 25 | "@/*": ["*"] 26 | } 27 | }, 28 | "include": ["src/**/*.ts", "src/**/*.json", ".env"], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /plugins/git-proxy-plugin-samples/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a sample plugin that logs a message when the pull action is called. It is written using 3 | * ES modules to demonstrate the use of ESM in plugins. 4 | */ 5 | 6 | // Peer dependencies; its expected that these deps exist on Node module path if you've installed @finos/git-proxy 7 | import { PullActionPlugin } from '@finos/git-proxy/src/plugin.js'; 8 | import { Step } from '@finos/git-proxy/src/proxy/actions/index.js'; 9 | 10 | class RunOnPullPlugin extends PullActionPlugin { 11 | constructor() { 12 | super(function logMessage(req, action) { 13 | const step = new Step('RunOnPullPlugin'); 14 | action.addStep(step); 15 | console.log('RunOnPullPlugin: Received fetch request', req.url); 16 | return action; 17 | }); 18 | } 19 | } 20 | 21 | // Default exports are supported and will be loaded by the plugin loader 22 | export default new RunOnPullPlugin(); 23 | -------------------------------------------------------------------------------- /src/config/file.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { join } from 'path'; 3 | import { Convert } from './generated/config'; 4 | 5 | let configFile: string = join(__dirname, '../../proxy.config.json'); 6 | 7 | /** 8 | * Sets the path to the configuration file. 9 | * 10 | * @param {string} file - The path to the configuration file. 11 | * @return {void} 12 | */ 13 | export function setConfigFile(file: string) { 14 | configFile = file; 15 | } 16 | 17 | /** 18 | * Gets the path to the current configuration file. 19 | * 20 | * @return {string} file - The path to the configuration file. 21 | */ 22 | export function getConfigFile() { 23 | return configFile; 24 | } 25 | 26 | export function validate(filePath: string = configFile): boolean { 27 | // Use QuickType to validate the configuration 28 | const configContent = readFileSync(filePath, 'utf-8'); 29 | Convert.toGitProxyConfig(configContent); 30 | return true; 31 | } 32 | -------------------------------------------------------------------------------- /experimental/license-inventory/dev/tempo.yaml: -------------------------------------------------------------------------------- 1 | stream_over_http_enabled: true 2 | server: 3 | http_listen_port: 3200 4 | 5 | distributor: 6 | receivers: 7 | otlp: 8 | protocols: 9 | http: 10 | 11 | compactor: 12 | compaction: 13 | block_retention: 1h 14 | 15 | metrics_generator: 16 | registry: 17 | external_labels: 18 | source: tempo 19 | cluster: docker-compose 20 | storage: 21 | path: /var/tempo/generator/wal 22 | remote_write: 23 | - url: http://prometheus:9090/api/v1/write 24 | send_exemplars: true 25 | traces_storage: 26 | path: /var/tempo/generator/traces 27 | 28 | storage: 29 | trace: 30 | backend: local 31 | wal: 32 | path: /var/tempo/wal 33 | local: 34 | path: /var/tempo/blocks 35 | 36 | overrides: 37 | defaults: 38 | metrics_generator: 39 | processors: [service-graphs, span-metrics, local-blocks] 40 | generate_native_histograms: both 41 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/test/globalSetup.ts: -------------------------------------------------------------------------------- 1 | import { MongoMemoryServer } from 'mongodb-memory-server-core'; 2 | import * as mongoose from 'mongoose'; 3 | import { config } from './utils/config'; 4 | 5 | export default async function globalSetup() { 6 | // TODO: make this logic smarter for no mongo, existing mongo, or using MongoMemoryServer 7 | if (config.Memory) { 8 | const instance = await MongoMemoryServer.create(); 9 | const uri = instance.getUri(); 10 | global.__MONGOINSTANCE = instance; 11 | process.env.MONGO_URI = uri.slice(0, uri.lastIndexOf('/')); 12 | 13 | // The following is to make sure the database is clean before a test suite starts 14 | const conn = await mongoose.connect(`${process.env.MONGO_URI}/${config.Database}`); 15 | await conn.connection.db?.dropDatabase(); 16 | await mongoose.disconnect(); 17 | } 18 | 19 | if (typeof process.env.MONGO_URI !== 'string') { 20 | // pass env validation 21 | process.env.MONGO_URI = 'dummy'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/fixtures/captured-push.bin-README.md: -------------------------------------------------------------------------------- 1 | # Captured push data 2 | 3 | The captured-push.bin file contains a single captured push request used to test push file parsing. It was captured by adding the following to the `exec` function in _src/proxy/processors/push-action/parsePush.ts_: 4 | 5 | ```typescript 6 | fs.writeFileSync('./.tmp/captured-pack.bin', req.body); 7 | ``` 8 | 9 | The push that was captured was generated by checking out the git proxy repository (a second copy) and running the following in its root directory to generate changes to commit: 10 | 11 | ```bash 12 | echo "New content for pack testing $(date)" > new-file.txt 13 | echo "# Updated README $(date)" >> README.md 14 | git add . 15 | git commit -m "test: test commit for pack capture $(date)" 16 | ``` 17 | 18 | The commit was then pushed into a locally running copy of git proxy (where the committing user was added as a contributor to the git-proxy repo). The push is processed as normally and its body written to file by the above modification. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /experimental/license-inventory/dev/grafana/datasources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Loki 5 | type: loki 6 | uid: loki 7 | access: proxy 8 | url: http://loki:3100 9 | basicAuth: false 10 | version: 1 11 | editable: false 12 | isDefault: true 13 | 14 | - name: Prometheus 15 | type: prometheus 16 | uid: prometheus 17 | access: proxy 18 | orgId: 1 19 | url: http://prometheus:9090 20 | basicAuth: false 21 | isDefault: false 22 | version: 1 23 | editable: false 24 | jsonData: 25 | httpMethod: GET 26 | 27 | - name: Tempo 28 | type: tempo 29 | uid: tempo 30 | access: proxy 31 | orgId: 1 32 | url: http://tempo:3200 33 | basicAuth: false 34 | isDefault: false 35 | version: 1 36 | editable: false 37 | jsonData: 38 | tracesToLogsV2: 39 | datasourceUid: loki 40 | filterByTraceID: true 41 | filterBySpanID: true 42 | tracesToMetrics: 43 | datasourceUid: prometheus 44 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom'; 4 | import { AuthProvider } from './ui/auth/AuthProvider'; 5 | 6 | // core components 7 | import Dashboard from './ui/layouts/Dashboard'; 8 | import Login from './ui/views/Login/Login'; 9 | import './ui/assets/css/material-dashboard-react.css'; 10 | import NotAuthorized from './ui/views/Extras/NotAuthorized'; 11 | import NotFound from './ui/views/Extras/NotFound'; 12 | 13 | ReactDOM.render( 14 | 15 | 16 | 17 | } /> 18 | } /> 19 | } /> 20 | } /> 21 | } /> 22 | 23 | 24 | , 25 | document.getElementById('root'), 26 | ); 27 | -------------------------------------------------------------------------------- /src/ui/components/Card/CardBody.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import styles from '../../assets/jss/material-dashboard-react/components/cardBodyStyle'; 5 | 6 | const useStyles = makeStyles(styles); 7 | 8 | interface CardBodyProps extends React.ComponentProps<'div'> { 9 | className?: string; 10 | plain?: boolean; 11 | profile?: boolean; 12 | children?: React.ReactNode; 13 | } 14 | 15 | const CardBody: React.FC = ({ 16 | className = '', 17 | children, 18 | plain = false, 19 | profile = false, 20 | ...rest 21 | }) => { 22 | const classes = useStyles(); 23 | 24 | const cardBodyClasses = clsx({ 25 | [classes.cardBody]: true, 26 | [classes.cardBodyPlain]: plain, 27 | [classes.cardBodyProfile]: profile, 28 | [className]: className, 29 | }); 30 | 31 | return ( 32 |
33 | {children} 34 |
35 | ); 36 | }; 37 | 38 | export default CardBody; 39 | -------------------------------------------------------------------------------- /src/ui/components/Card/CardAvatar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import styles from '../../assets/jss/material-dashboard-react/components/cardAvatarStyle'; 5 | 6 | const useStyles = makeStyles(styles); 7 | 8 | interface CardAvatarProps extends React.ComponentProps<'div'> { 9 | children: React.ReactNode; 10 | className?: string; 11 | profile?: boolean; 12 | plain?: boolean; 13 | } 14 | 15 | const CardAvatar: React.FC = ({ 16 | children, 17 | className = '', 18 | profile = false, 19 | plain = false, 20 | ...rest 21 | }) => { 22 | const classes = useStyles(); 23 | 24 | const cardAvatarClasses = clsx({ 25 | [classes.cardAvatar]: true, 26 | [classes.cardAvatarProfile]: profile, 27 | [classes.cardAvatarPlain]: plain, 28 | [className]: className, 29 | }); 30 | 31 | return ( 32 |
33 | {children} 34 |
35 | ); 36 | }; 37 | 38 | export default CardAvatar; 39 | -------------------------------------------------------------------------------- /src/ui/components/Card/CardIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import styles from '../../assets/jss/material-dashboard-react/components/cardIconStyle'; 5 | 6 | const useStyles = makeStyles(styles); 7 | 8 | type CardIconColor = 'warning' | 'success' | 'danger' | 'info' | 'primary' | 'rose'; 9 | 10 | interface CardIconProps { 11 | className?: string; 12 | color?: CardIconColor; 13 | children?: React.ReactNode; 14 | [key: string]: any; 15 | } 16 | 17 | const CardIcon: React.FC = (props) => { 18 | const classes = useStyles(); 19 | const { className, children, color, ...rest } = props; 20 | 21 | const cardIconClasses = clsx({ 22 | [classes.cardIcon]: true, 23 | [color ? classes[`${color}CardHeader`] : '']: color, 24 | [className || '']: className !== undefined, 25 | }); 26 | 27 | return ( 28 |
29 | {children} 30 |
31 | ); 32 | }; 33 | 34 | export default CardIcon; 35 | -------------------------------------------------------------------------------- /src/ui/assets/jss/material-dashboard-react/tooltipStyle.js: -------------------------------------------------------------------------------- 1 | import { blackColor, hexToRgb } from '../material-dashboard-react.js'; 2 | 3 | const tooltipStyle = { 4 | tooltip: { 5 | padding: '10px 15px', 6 | minWidth: '130px', 7 | lineHeight: '1.7em', 8 | border: 'none', 9 | borderRadius: '3px', 10 | boxShadow: 11 | '0 8px 10px 1px rgba(' + 12 | hexToRgb(blackColor) + 13 | ', 0.14), 0 3px 14px 2px rgba(' + 14 | hexToRgb(blackColor) + 15 | ', 0.12), 0 5px 5px -3px rgba(' + 16 | hexToRgb(blackColor) + 17 | ', 0.2)', 18 | maxWidth: '200px', 19 | textAlign: 'center', 20 | fontFamily: '"Helvetica Neue",Helvetica,Arial,sans-serif', 21 | fontSize: '12px', 22 | fontStyle: 'normal', 23 | fontWeight: '400', 24 | textShadow: 'none', 25 | textTransform: 'none', 26 | letterSpacing: 'normal', 27 | wordBreak: 'normal', 28 | wordSpacing: 'normal', 29 | wordWrap: 'normal', 30 | whiteSpace: 'normal', 31 | lineBreak: 'auto', 32 | }, 33 | }; 34 | export default tooltipStyle; 35 | -------------------------------------------------------------------------------- /src/ui/components/Card/Card.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import styles from '../../assets/jss/material-dashboard-react/components/cardStyle'; 5 | 6 | const useStyles = makeStyles(styles); 7 | 8 | interface CardProps extends React.ComponentProps<'div'> { 9 | className?: string; 10 | plain?: boolean; 11 | profile?: boolean; 12 | chart?: boolean; 13 | children?: React.ReactNode; 14 | } 15 | 16 | const Card: React.FC = ({ 17 | className = '', 18 | children, 19 | plain = false, 20 | profile = false, 21 | chart = false, 22 | ...rest 23 | }) => { 24 | const classes = useStyles(); 25 | 26 | const cardClasses = clsx({ 27 | [classes.card]: true, 28 | [classes.cardPlain]: plain, 29 | [classes.cardProfile]: profile, 30 | [classes.cardChart]: chart, 31 | [className]: className, 32 | }); 33 | 34 | return ( 35 |
36 | {children} 37 |
38 | ); 39 | }; 40 | 41 | export default Card; 42 | -------------------------------------------------------------------------------- /src/proxy/actions/autoActions.ts: -------------------------------------------------------------------------------- 1 | import { authorise, reject } from '../../db'; 2 | import { Action } from './Action'; 3 | 4 | const attemptAutoApproval = async (action: Action) => { 5 | try { 6 | const attestation = { 7 | timestamp: new Date(), 8 | autoApproved: true, 9 | }; 10 | await authorise(action.id, attestation); 11 | console.log('Push automatically approved by system.'); 12 | 13 | return true; 14 | } catch (error: any) { 15 | console.error('Error during auto-approval:', error.message); 16 | return false; 17 | } 18 | }; 19 | 20 | const attemptAutoRejection = async (action: Action) => { 21 | try { 22 | const attestation = { 23 | timestamp: new Date(), 24 | autoApproved: true, 25 | }; 26 | await reject(action.id, attestation); 27 | console.log('Push automatically rejected by system.'); 28 | 29 | return true; 30 | } catch (error: any) { 31 | console.error('Error during auto-rejection:', error.message); 32 | return false; 33 | } 34 | }; 35 | 36 | export { attemptAutoApproval, attemptAutoRejection }; 37 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | name: 'Dependency Review' 2 | on: [pull_request] 3 | 4 | permissions: 5 | contents: read 6 | pull-requests: write 7 | 8 | jobs: 9 | dependency-review: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Harden Runner 13 | uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2 14 | with: 15 | egress-policy: audit 16 | 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 19 | - name: Dependency Review 20 | uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4 21 | with: 22 | comment-summary-in-pr: always 23 | fail-on-severity: high 24 | allow-licenses: MIT, MIT-0, Apache-2.0, BSD-3-Clause, BSD-3-Clause-Clear, ISC, BSD-2-Clause, Unlicense, CC0-1.0, 0BSD, X11, MPL-2.0, MPL-1.0, MPL-1.1, MPL-2.0, OFL-1.1, Zlib, BlueOak-1.0.0 25 | fail-on-scopes: development, runtime 26 | allow-dependencies-licenses: 'pkg:npm/caniuse-lite' 27 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/db/schemas/license/license.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose'; 2 | import { calSchema, calValidation } from './chooseALicense'; 3 | import { z } from 'zod'; 4 | import { Mongoosify } from '@/db/types'; 5 | 6 | export const licenseValidation = z.object({ 7 | id: z.string().uuid(), 8 | name: z.string(), 9 | spdxID: z.string().optional(), 10 | chooseALicenseInfo: calValidation.optional(), 11 | }); 12 | export type LicenseValidation = z.infer; 13 | 14 | export type LicenseSchema = Mongoosify; 15 | export const licenseSchema = new Schema( 16 | { 17 | _id: { type: Schema.Types.UUID, required: true }, 18 | // pretty name 19 | name: { type: String, required: true }, 20 | // allow for licenses which don't have an SPDX ID 21 | spdxID: String, 22 | chooseALicenseInfo: calSchema, 23 | }, 24 | { 25 | // automatic createdAt updatedAt 26 | timestamps: true, 27 | }, 28 | ); 29 | 30 | // licenses collection 31 | export const License = model('License', licenseSchema); 32 | -------------------------------------------------------------------------------- /experimental/li-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@finos/git-proxy-li-cli", 3 | "version": "0.0.1", 4 | "author": "git-proxy contributors", 5 | "license": "Apache-2.0", 6 | "files": [ 7 | "dist" 8 | ], 9 | "scripts": { 10 | "start": "ts-node src/cli.ts", 11 | "build": "rimraf dist && tsc --project tsconfig.publish.json && tsc-alias && chmod u+x ./dist/cli.js", 12 | "type-check": "tsc --noEmit", 13 | "test": "jest --forceExit --detectOpenHandles", 14 | "update-local-licenses": "wget https://spdx.org/licenses/licenses.json -O src/lib/licenses.json" 15 | }, 16 | "dependencies": { 17 | "@inquirer/prompts": "^7.8.6", 18 | "yaml": "^2.8.1", 19 | "yargs": "^17.7.2", 20 | "zod": "^3.25.76" 21 | }, 22 | "devDependencies": { 23 | "@jest/globals": "^29.7.0", 24 | "@types/node": "^22.18.7", 25 | "@types/yargs": "^17.0.33", 26 | "jest": "^29.7.0", 27 | "rimraf": "^6.0.1", 28 | "ts-jest": "^29.4.4", 29 | "ts-node": "^10.9.2", 30 | "tsc-alias": "^1.8.16", 31 | "tslib": "^2.8.1", 32 | "typescript": "^5.9.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /scripts/prepare.js: -------------------------------------------------------------------------------- 1 | // ======= 2 | // Import. 3 | // ======= 4 | 5 | const { execSync } = require('child_process'); 6 | const { existsSync } = require('fs'); 7 | 8 | // =========== 9 | // File paths. 10 | // =========== 11 | 12 | const FILE_COMMIT = './.husky/commit-msg'; 13 | const FILE_HUSKY = './.husky/_/husky.sh'; 14 | 15 | // ========= 16 | // Commands. 17 | // ========= 18 | 19 | const COMMIT_MSG_STRING = "'npx --no -- commitlint --edit $'{1}''"; 20 | const CLI_COMMIT = `npx husky add .husky/commit-msg ${COMMIT_MSG_STRING}`; 21 | const CLI_HUSKY = 'npx husky install'; 22 | 23 | // ============== 24 | // Husky install. 25 | // ============== 26 | 27 | // this will create .husky/_/husky.sh if it does not yet exist. 28 | if (!existsSync(FILE_HUSKY)) { 29 | global.console.log(CLI_HUSKY); 30 | execSync(CLI_HUSKY); 31 | } 32 | 33 | // ==================== 34 | // Add pre-commit hook. 35 | // ==================== 36 | 37 | // this will create .husky/commit-msg if it does not yet exist. 38 | if (!existsSync(FILE_COMMIT)) { 39 | global.console.log(CLI_COMMIT); 40 | execSync(CLI_COMMIT); 41 | } 42 | -------------------------------------------------------------------------------- /experimental/license-inventory/dev/loki.yaml: -------------------------------------------------------------------------------- 1 | auth_enabled: false 2 | 3 | server: 4 | http_listen_port: 3100 5 | grpc_listen_port: 9096 6 | 7 | common: 8 | instance_addr: 127.0.0.1 9 | path_prefix: /tmp/loki 10 | storage: 11 | filesystem: 12 | chunks_directory: /tmp/loki/chunks 13 | rules_directory: /tmp/loki/rules 14 | replication_factor: 1 15 | ring: 16 | kvstore: 17 | store: inmemory 18 | 19 | frontend: 20 | max_outstanding_per_tenant: 2048 21 | 22 | pattern_ingester: 23 | enabled: true 24 | 25 | limits_config: 26 | max_global_streams_per_user: 0 27 | ingestion_rate_mb: 50000 28 | ingestion_burst_size_mb: 50000 29 | volume_enabled: true 30 | 31 | query_range: 32 | results_cache: 33 | cache: 34 | embedded_cache: 35 | enabled: true 36 | max_size_mb: 100 37 | 38 | schema_config: 39 | configs: 40 | - from: 2020-10-24 41 | store: tsdb 42 | object_store: filesystem 43 | schema: v13 44 | index: 45 | prefix: index_ 46 | period: 24h 47 | 48 | analytics: 49 | reporting_enabled: false 50 | -------------------------------------------------------------------------------- /src/ui/assets/jss/material-dashboard-react/components/cardStyle.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from '@material-ui/core/styles'; 2 | import { blackColor, whiteColor, hexToRgb } from '../../material-dashboard-react'; 3 | 4 | const cardStyle = createStyles({ 5 | card: { 6 | border: '0', 7 | marginBottom: '30px', 8 | marginTop: '30px', 9 | borderRadius: '6px', 10 | color: `rgba(${hexToRgb(blackColor)}, 0.87)`, 11 | background: whiteColor, 12 | width: '100%', 13 | boxShadow: `0 1px 4px 0 rgba(${hexToRgb(blackColor)}, 0.14)`, 14 | position: 'relative' as const, 15 | display: 'flex' as const, 16 | flexDirection: 'column' as const, 17 | minWidth: '0', 18 | wordWrap: 'break-word' as const, 19 | fontSize: '.875rem', 20 | }, 21 | cardPlain: { 22 | background: 'transparent', 23 | boxShadow: 'none', 24 | }, 25 | cardProfile: { 26 | marginTop: '30px', 27 | textAlign: 'center' as const, 28 | }, 29 | cardChart: { 30 | '& p': { 31 | marginTop: '0px', 32 | paddingTop: '0px', 33 | }, 34 | }, 35 | }); 36 | 37 | export default cardStyle; 38 | -------------------------------------------------------------------------------- /src/ui/assets/jss/material-dashboard-react/views/iconsStyle.js: -------------------------------------------------------------------------------- 1 | import { boxShadow, whiteColor, grayColor, hexToRgb } from '../../jss/material-dashboard-react.js'; 2 | 3 | const iconsStyle = { 4 | iframe: { 5 | width: '100%', 6 | height: '500px', 7 | border: '0', 8 | ...boxShadow, 9 | }, 10 | iframeContainer: { 11 | margin: '0 -20px 0', 12 | }, 13 | cardCategoryWhite: { 14 | '&,& a,& a:hover,& a:focus': { 15 | color: 'rgba(' + hexToRgb(whiteColor) + ',.62)', 16 | margin: '0', 17 | fontSize: '14px', 18 | marginTop: '0', 19 | marginBottom: '0', 20 | }, 21 | '& a,& a:hover,& a:focus': { 22 | color: whiteColor, 23 | }, 24 | }, 25 | cardTitleWhite: { 26 | color: whiteColor, 27 | marginTop: '0px', 28 | minHeight: 'auto', 29 | fontWeight: '300', 30 | fontFamily: "'Roboto', 'Helvetica', 'Arial', sans-serif", 31 | marginBottom: '3px', 32 | textDecoration: 'none', 33 | '& small': { 34 | color: grayColor[1], 35 | fontWeight: '400', 36 | lineHeight: '1', 37 | }, 38 | }, 39 | }; 40 | 41 | export default iconsStyle; 42 | -------------------------------------------------------------------------------- /.github/workflows/unused-dependencies.yml: -------------------------------------------------------------------------------- 1 | name: 'Unused Dependencies' 2 | on: [pull_request] 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | unused-dependecies: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Harden Runner 12 | uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2 13 | with: 14 | egress-policy: audit 15 | 16 | - name: 'Checkout Repository' 17 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 18 | - name: 'Setup Node.js' 19 | uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 20 | with: 21 | node-version: '22.x' 22 | - name: 'Run depcheck' 23 | run: | 24 | npx depcheck --skip-missing --ignores="tsx,@babel/*,@commitlint/*,eslint,eslint-*,husky,ts-node,concurrently,nyc,prettier,typescript,tsconfig-paths,vite-tsconfig-paths,quicktype,history,@types/domutils,@vitest/coverage-v8" 25 | echo $? 26 | if [[ $? == 1 ]]; then 27 | echo "Unused dependencies or devDependencies found" 28 | exit 1 29 | fi 30 | -------------------------------------------------------------------------------- /website/src/components/avatar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * Avatar component 6 | * @param {*} props 7 | * @return {JSX.Element} 8 | */ 9 | export default function Avatar({ name, description, username }) { 10 | // add prop validation 11 | Avatar.propTypes = { 12 | name: PropTypes.string.isRequired, 13 | description: PropTypes.string.isRequired, 14 | username: PropTypes.string.isRequired, 15 | }; 16 | 17 | const profileUrl = `https://github.com/${username}`; 18 | const imageUrl = `${profileUrl}.png`; 19 | 20 | return ( 21 |
22 | 23 |
24 |
{name}
25 | {description} 26 | 27 | 28 | @{username} 29 | 30 | 31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/ui/auth/AuthProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect } from 'react'; 2 | import { getUserInfo } from '../services/auth'; 3 | import { PublicUser } from '../../db/types'; 4 | import { AuthContext } from '../context'; 5 | 6 | export const AuthProvider: React.FC> = ({ children }) => { 7 | const [user, setUser] = useState(null); 8 | const [isLoading, setIsLoading] = useState(true); 9 | 10 | const refreshUser = async () => { 11 | try { 12 | const data = await getUserInfo(); 13 | setUser(data); 14 | } catch (error) { 15 | setUser(null); 16 | } finally { 17 | setIsLoading(false); 18 | } 19 | }; 20 | 21 | useEffect(() => { 22 | refreshUser(); 23 | }, []); 24 | 25 | return ( 26 | 27 | {children} 28 | 29 | ); 30 | }; 31 | 32 | export const useAuth = () => { 33 | const context = useContext(AuthContext); 34 | if (!context) { 35 | throw new Error('useAuth must be used within an AuthProvider'); 36 | } 37 | return context; 38 | }; 39 | -------------------------------------------------------------------------------- /src/ui/components/Search/Search.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TextField } from '@material-ui/core'; 3 | import './Search.css'; 4 | import InputAdornment from '@material-ui/core/InputAdornment'; 5 | import SearchIcon from '@material-ui/icons/Search'; 6 | 7 | interface SearchProps { 8 | onSearch: (query: string) => void; 9 | placeholder?: string; 10 | } 11 | 12 | const Search: React.FC = ({ onSearch, placeholder = 'Search...' }) => { 13 | const handleSearchChange = (event: React.ChangeEvent) => { 14 | const query = event.target.value; 15 | onSearch(query); 16 | }; 17 | 18 | return ( 19 |
20 | 30 | 31 | 32 | ), 33 | }} 34 | /> 35 |
36 | ); 37 | }; 38 | 39 | export default Search; 40 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "docusaurus": "docusaurus", 4 | "deploy": "docusaurus deploy", 5 | "serve": "docusaurus serve", 6 | "clear": "docusaurus clear", 7 | "start": "docusaurus start", 8 | "swizzle": "docusaurus swizzle", 9 | "build": "docusaurus build", 10 | "publish-gh-pages": "docusaurus deploy" 11 | }, 12 | "dependencies": { 13 | "@docusaurus/core": "^3.8.1", 14 | "@docusaurus/plugin-google-gtag": "^3.8.1", 15 | "@docusaurus/preset-classic": "^3.8.1", 16 | "axios": "^1.12.2", 17 | "classnames": "^2.5.1", 18 | "clsx": "^2.1.1", 19 | "eslint": "^9.36.0", 20 | "eslint-plugin-react": "^7.37.5", 21 | "react": "^19.1.1", 22 | "react-dom": "^19.1.1", 23 | "react-player": "^3.3.3", 24 | "react-slick": "^0.31.0", 25 | "react-social-media-embed": "^2.5.18", 26 | "slick-carousel": "^1.8.1" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.5%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ui/components/Card/CardFooter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import styles from '../../assets/jss/material-dashboard-react/components/cardFooterStyle'; 5 | 6 | const useStyles = makeStyles(styles); 7 | 8 | interface CardFooterProps extends React.ComponentProps<'div'> { 9 | className?: string; 10 | plain?: boolean; 11 | profile?: boolean; 12 | stats?: boolean; 13 | chart?: boolean; 14 | children?: React.ReactNode; 15 | } 16 | 17 | const CardFooter: React.FC = ({ 18 | className, 19 | children, 20 | plain, 21 | profile, 22 | stats, 23 | chart, 24 | ...rest 25 | }) => { 26 | const classes = useStyles(); 27 | 28 | const cardFooterClasses = clsx({ 29 | [classes.cardFooter]: true, 30 | [classes.cardFooterPlain]: plain, 31 | [classes.cardFooterProfile]: profile, 32 | [classes.cardFooterStats]: stats, 33 | [classes.cardFooterChart]: chart, 34 | [className || '']: className !== undefined, 35 | }); 36 | 37 | return ( 38 |
39 | {children} 40 |
41 | ); 42 | }; 43 | 44 | export default CardFooter; 45 | -------------------------------------------------------------------------------- /src/ui/assets/jss/material-dashboard-react/layouts/dashboardStyle.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '@material-ui/core/styles/createTheme'; 2 | import { drawerWidth, transition, container } from '../../material-dashboard-react'; 3 | 4 | interface AppStyleProps { 5 | wrapper: React.CSSProperties; 6 | mainPanel: React.CSSProperties & { 7 | [key: string]: any; 8 | }; 9 | content: React.CSSProperties; 10 | container: typeof container; 11 | map: React.CSSProperties; 12 | } 13 | 14 | const appStyle = (theme: Theme): AppStyleProps => ({ 15 | wrapper: { 16 | position: 'relative', 17 | top: '0', 18 | height: '100vh', 19 | }, 20 | mainPanel: { 21 | [theme.breakpoints.up('md')]: { 22 | width: `calc(100% - ${drawerWidth}px)`, 23 | }, 24 | overflow: 'auto', 25 | position: 'relative', 26 | float: 'right', 27 | ...transition, 28 | maxHeight: '100%', 29 | width: '100%', 30 | WebkitOverflowScrolling: 'touch' as any, 31 | }, 32 | content: { 33 | marginTop: '70px', 34 | padding: '30px 15px', 35 | minHeight: 'calc(100vh - 123px)', 36 | }, 37 | container: { ...container }, 38 | map: { 39 | marginTop: '70px', 40 | }, 41 | }); 42 | 43 | export default appStyle; 44 | -------------------------------------------------------------------------------- /src/ui/components/Filtering/Filtering.css: -------------------------------------------------------------------------------- 1 | .filtering-container { 2 | position: relative; 3 | display: inline-block; 4 | padding-bottom: 10px; 5 | } 6 | 7 | .dropdown-toggle { 8 | padding: 10px 10px; 9 | padding-right: 10px; 10 | border: 1px solid #ccc; 11 | border-radius: 5px; 12 | background-color: #fff; 13 | color: #333; 14 | cursor: pointer; 15 | font-size: 14px; 16 | text-align: left; 17 | width: 130px; 18 | display: inline-flex; 19 | align-items: center; 20 | justify-content: space-between; 21 | } 22 | 23 | .dropdown-toggle:hover { 24 | background-color: #f0f0f0; 25 | } 26 | 27 | .dropdown-arrow { 28 | border: none; 29 | background: none; 30 | cursor: pointer; 31 | font-size: 15px; 32 | margin-left: 1px; 33 | margin-right: 10px; 34 | } 35 | 36 | .dropdown-menu { 37 | position: absolute; 38 | background-color: #fff; 39 | border: 1px solid #ccc; 40 | border-radius: 5px; 41 | margin-top: 5px; 42 | z-index: 1000; 43 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 44 | } 45 | 46 | .dropdown-item { 47 | padding: 10px 15px; 48 | cursor: pointer; 49 | font-size: 14px; 50 | color: #333; 51 | } 52 | 53 | .dropdown-item:hover { 54 | background-color: #f0f0f0; 55 | } 56 | -------------------------------------------------------------------------------- /plugins/git-proxy-plugin-samples/README.md: -------------------------------------------------------------------------------- 1 | # GitProxy plugins & samples 2 | 3 | GitProxy supports extensibility in the form of plugins. These plugins are specified via [configuration](https://git-proxy.finos.org/docs/category/configuration) as NPM packages or JavaScript code on disk. For each plugin configured, GitProxy will attempt to load each package or file as a standard [Node module](https://nodejs.org/api/modules.html). Plugin authors will create instances of the extension classes exposed by GitProxy and use these objects to implement custom functionality. 4 | 5 | For detailed documentation, please refer to the [GitProxy development resources on the project's site](https://git-proxy.finos.org/docs/development/plugins) 6 | 7 | ## Included plugins 8 | 9 | These plugins are maintained by the core GitProxy team. As a future roadmap item, organizations can choose to omit 10 | certain features of GitProxy by simply removing the dependency from a deployed version of the application. 11 | 12 | - `git-proxy-plugin-samples`: "hello world" examples of the GitProxy plugin system 13 | 14 | ## Contributing 15 | 16 | Please refer to the [CONTRIBUTING.md](https://git-proxy.finos.org/docs/development/contributing) file for information on how to contribute to the GitProxy project. 17 | -------------------------------------------------------------------------------- /experimental/license-inventory/dev/otel-collector-config.yaml: -------------------------------------------------------------------------------- 1 | extensions: 2 | health_check: 3 | pprof: 4 | endpoint: 127.0.0.1:1777 5 | zpages: 6 | endpoint: 127.0.0.1:55679 7 | 8 | receivers: 9 | otlp: 10 | protocols: 11 | http: 12 | endpoint: 0.0.0.0:4318 13 | 14 | # collect own metrics 15 | prometheus: 16 | config: 17 | scrape_configs: 18 | - job_name: otel-collector 19 | scrape_interval: 10s 20 | static_configs: 21 | - targets: 22 | - 127.0.0.1:8888 23 | 24 | exporters: 25 | loki: 26 | endpoint: http://loki:3100/loki/api/v1/push 27 | prometheus: 28 | endpoint: 127.0.0.1:8889 29 | namespace: otel-collector 30 | send_timestamps: true 31 | metric_expiration: 180m 32 | enable_open_metrics: true 33 | add_metric_suffixes: false 34 | resource_to_telemetry_conversion: 35 | enabled: true 36 | otlphttp: 37 | endpoint: http://tempo:4318 38 | 39 | processors: 40 | batch: 41 | 42 | service: 43 | pipelines: 44 | traces: 45 | receivers: [otlp] 46 | exporters: [otlphttp] 47 | metrics: 48 | receivers: [otlp] 49 | exporters: [prometheus] 50 | logs: 51 | receivers: [otlp] 52 | exporters: [loki] 53 | -------------------------------------------------------------------------------- /src/ui/assets/jss/material-dashboard-react/components/typographyStyle.js: -------------------------------------------------------------------------------- 1 | import { 2 | defaultFont, 3 | primaryColor, 4 | infoColor, 5 | successColor, 6 | warningColor, 7 | dangerColor, 8 | grayColor, 9 | } from '../../material-dashboard-react.js'; 10 | 11 | const typographyStyle = { 12 | defaultFontStyle: { 13 | ...defaultFont, 14 | fontSize: '14px', 15 | }, 16 | defaultHeaderMargins: { 17 | marginTop: '20px', 18 | marginBottom: '10px', 19 | }, 20 | quote: { 21 | padding: '10px 20px', 22 | margin: '0 0 20px', 23 | fontSize: '17.5px', 24 | borderLeft: '5px solid ' + grayColor[10], 25 | }, 26 | quoteText: { 27 | margin: '0 0 10px', 28 | fontStyle: 'italic', 29 | }, 30 | quoteAuthor: { 31 | display: 'block', 32 | fontSize: '80%', 33 | lineHeight: '1.42857143', 34 | color: grayColor[1], 35 | }, 36 | mutedText: { 37 | color: grayColor[1], 38 | }, 39 | primaryText: { 40 | color: primaryColor[0], 41 | }, 42 | infoText: { 43 | color: infoColor[0], 44 | }, 45 | successText: { 46 | color: successColor[0], 47 | }, 48 | warningText: { 49 | color: warningColor[0], 50 | }, 51 | dangerText: { 52 | color: dangerColor[0], 53 | }, 54 | }; 55 | 56 | export default typographyStyle; 57 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { logger } from '@/logger'; 3 | import createApiRouter from '@/routes/api'; 4 | import pinoHTTP from 'pino-http'; 5 | import bodyParser from 'body-parser'; 6 | import { rateLimit } from 'express-rate-limit'; 7 | import helmet from 'helmet'; 8 | import { LicenseDataService } from './services/data'; 9 | // import lusca from 'lusca'; 10 | 11 | // helmet and lusca comparison 12 | // https://github.com/krakenjs/lusca/issues/42#issuecomment-65093906 13 | // TODO: integrate lusca once added sessions/auth 14 | 15 | const createApp = (lds: LicenseDataService) => { 16 | const app = express(); 17 | 18 | const limiter = rateLimit({ 19 | windowMs: 15 * 60 * 1000, 20 | limit: 100, 21 | standardHeaders: 'draft-7', 22 | legacyHeaders: false, 23 | // in memory store 24 | }); 25 | 26 | app.use(helmet()); 27 | app.use(limiter); 28 | app.use(bodyParser.json()); 29 | app.use( 30 | pinoHTTP({ 31 | logger, 32 | autoLogging: process.env.NODE_ENV === 'development', 33 | // overrides core logger redaction 34 | // please update in logger.ts 35 | // redact: [], 36 | }), 37 | ); 38 | 39 | app.use('/api', createApiRouter(lds)); 40 | return app; 41 | }; 42 | export { createApp }; 43 | -------------------------------------------------------------------------------- /src/ui/assets/jss/material-dashboard-react/checkboxAdnRadioStyle.js: -------------------------------------------------------------------------------- 1 | import { primaryColor, blackColor, hexToRgb } from '../material-dashboard-react.js'; 2 | 3 | const checkboxAdnRadioStyle = { 4 | root: { 5 | padding: '13px', 6 | '&:hover': { 7 | backgroundColor: 'unset', 8 | }, 9 | }, 10 | labelRoot: { 11 | marginLeft: '-14px', 12 | }, 13 | checked: { 14 | color: primaryColor[0] + '!important', 15 | }, 16 | checkedIcon: { 17 | width: '20px', 18 | height: '20px', 19 | border: '1px solid rgba(' + hexToRgb(blackColor) + ', .54)', 20 | borderRadius: '3px', 21 | }, 22 | uncheckedIcon: { 23 | width: '0px', 24 | height: '0px', 25 | padding: '10px', 26 | border: '1px solid rgba(' + hexToRgb(blackColor) + ', .54)', 27 | borderRadius: '3px', 28 | }, 29 | radio: { 30 | color: primaryColor[0] + '!important', 31 | }, 32 | radioChecked: { 33 | width: '20px', 34 | height: '20px', 35 | border: '1px solid ' + primaryColor[0], 36 | borderRadius: '50%', 37 | }, 38 | radioUnchecked: { 39 | width: '0px', 40 | height: '0px', 41 | padding: '10px', 42 | border: '1px solid rgba(' + hexToRgb(blackColor) + ', .54)', 43 | borderRadius: '50%', 44 | }, 45 | }; 46 | 47 | export default checkboxAdnRadioStyle; 48 | -------------------------------------------------------------------------------- /src/ui/components/Card/CardHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import styles from '../../assets/jss/material-dashboard-react/components/cardHeaderStyle'; 5 | 6 | const useStyles = makeStyles(styles); 7 | 8 | export type CardHeaderColor = 'warning' | 'success' | 'danger' | 'info' | 'primary' | 'rose'; 9 | 10 | interface CardHeaderProps extends React.ComponentProps<'div'> { 11 | className?: string; 12 | color?: CardHeaderColor; 13 | plain?: boolean; 14 | stats?: boolean; 15 | icon?: boolean; 16 | children?: React.ReactNode; 17 | } 18 | 19 | const CardHeader: React.FC = (props) => { 20 | const classes = useStyles(); 21 | const { className, children, color, plain, stats, icon, ...rest } = props; 22 | 23 | const cardHeaderClasses = clsx({ 24 | [classes.cardHeader]: true, 25 | [color ? classes[`${color}CardHeader`] : '']: color, 26 | [classes.cardHeaderPlain]: plain, 27 | [classes.cardHeaderStats]: stats, 28 | [classes.cardHeaderIcon]: icon, 29 | [className || '']: className !== undefined, 30 | }); 31 | 32 | return ( 33 |
34 | {children} 35 |
36 | ); 37 | }; 38 | 39 | export default CardHeader; 40 | -------------------------------------------------------------------------------- /src/ui/assets/jss/material-dashboard-react/components/cardFooterStyle.js: -------------------------------------------------------------------------------- 1 | import { grayColor } from '../../material-dashboard-react.js'; 2 | 3 | const cardFooterStyle = { 4 | cardFooter: { 5 | padding: '0', 6 | paddingTop: '10px', 7 | margin: '0 15px 10px', 8 | borderRadius: '0', 9 | justifyContent: 'space-between', 10 | alignItems: 'center', 11 | display: 'flex', 12 | backgroundColor: 'transparent', 13 | border: '0', 14 | }, 15 | cardFooterProfile: { 16 | marginTop: '-15px', 17 | }, 18 | cardFooterPlain: { 19 | paddingLeft: '5px', 20 | paddingRight: '5px', 21 | backgroundColor: 'transparent', 22 | }, 23 | cardFooterStats: { 24 | borderTop: '1px solid ' + grayColor[10], 25 | marginTop: '20px', 26 | '& svg': { 27 | position: 'relative', 28 | top: '4px', 29 | marginRight: '3px', 30 | marginLeft: '3px', 31 | width: '16px', 32 | height: '16px', 33 | }, 34 | '& .fab,& .fas,& .far,& .fal,& .material-icons': { 35 | fontSize: '16px', 36 | position: 'relative', 37 | top: '4px', 38 | marginRight: '3px', 39 | marginLeft: '3px', 40 | }, 41 | }, 42 | cardFooterChart: { 43 | borderTop: '1px solid ' + grayColor[10], 44 | }, 45 | }; 46 | 47 | export default cardFooterStyle; 48 | -------------------------------------------------------------------------------- /scripts/doc-schema.js: -------------------------------------------------------------------------------- 1 | const { execFileSync } = require('child_process'); 2 | const { writeFileSync, readFileSync, mkdtempSync } = require('fs'); 3 | const { tmpdir } = require('os'); 4 | const { sep } = require('path'); 5 | const JSFH_CONFIG = './jsfh.config.json'; 6 | const SCHEMA_FILE = './config.schema.json'; 7 | const OUTPUT_PATH = './website/docs/configuration/reference.mdx'; 8 | 9 | try { 10 | const osTempdir = tmpdir(); 11 | const tempdir = mkdtempSync(`${osTempdir}${sep}`); 12 | 13 | const genDocOutput = execFileSync('generate-schema-doc', [ 14 | '--config-file', 15 | JSFH_CONFIG, 16 | SCHEMA_FILE, 17 | `${tempdir}${sep}schema.md`, 18 | ]).toString('utf-8'); 19 | console.log(genDocOutput); 20 | 21 | const schemaDoc = readFileSync(`${tempdir}${sep}schema.md`, 'utf-8') 22 | .replace(/\s\s\n\n<\/summary>/g, '\n') 23 | .replace(/# GitProxy configuration file/g, '# Schema Reference'); // https://github.com/finos/git-proxy/pull/327#discussion_r1377343213 24 | const docString = `--- 25 | title: Schema Reference 26 | description: JSON schema reference documentation for GitProxy 27 | --- 28 | 29 | ${schemaDoc} 30 | `; 31 | writeFileSync(OUTPUT_PATH, docString); 32 | console.log(`Wrote schema reference to ${OUTPUT_PATH}`); 33 | } catch (err) { 34 | console.error(err); 35 | } 36 | -------------------------------------------------------------------------------- /src/ui/components/Pagination/Pagination.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Pagination.css'; 3 | 4 | interface PaginationProps { 5 | currentPage: number; 6 | totalItems?: number; 7 | itemsPerPage: number; 8 | onPageChange: (page: number) => void; 9 | } 10 | 11 | const Pagination: React.FC = ({ 12 | currentPage, 13 | totalItems = 0, 14 | itemsPerPage, 15 | onPageChange, 16 | }) => { 17 | const totalPages = Math.ceil(totalItems / itemsPerPage); 18 | 19 | const handlePageClick = (page: number) => { 20 | if (page >= 1 && page <= totalPages) { 21 | onPageChange(page); 22 | } 23 | }; 24 | 25 | return ( 26 |
27 | 34 | 35 | Page {totalPages === 0 ? '0 of 0' : `${currentPage} of ${totalPages}`} 36 | 37 | 44 |
45 | ); 46 | }; 47 | 48 | export default Pagination; 49 | -------------------------------------------------------------------------------- /.github/workflows/sample-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish samples to NPM 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'sample-*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | build-and-publish: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Harden Runner 16 | uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 17 | with: 18 | egress-policy: audit 19 | - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 20 | # Setup .npmrc file to publish to npm 21 | - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 22 | with: 23 | node-version: '22.x' 24 | registry-url: 'https://registry.npmjs.org' 25 | 26 | - name: Install dependencies 27 | working-directory: plugins/git-proxy-plugin-samples 28 | run: npm ci 29 | 30 | - name: Build TypeScript 31 | working-directory: plugins/git-proxy-plugin-samples 32 | run: npm run build 33 | 34 | - name: Install peers and publish 35 | working-directory: plugins/git-proxy-plugin-samples 36 | run: | 37 | npm install --include=peer 38 | npm publish --provenance --access=public 39 | env: 40 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 41 | -------------------------------------------------------------------------------- /src/ui/components/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import ListItem from '@material-ui/core/ListItem'; 4 | import List from '@material-ui/core/List'; 5 | import styles from '../../assets/jss/material-dashboard-react/components/footerStyle'; 6 | import { MarkGithubIcon } from '@primer/octicons-react'; 7 | 8 | const useStyles = makeStyles(styles); 9 | 10 | const Footer: React.FC = () => { 11 | const classes = useStyles(); 12 | 13 | return ( 14 |
15 |
16 |
17 | 18 | 19 | 25 | 26 | 27 | 28 | 29 |
30 |

31 | © {new Date().getFullYear()} GitProxy 32 |

33 |
34 |
35 | ); 36 | }; 37 | 38 | export default Footer; 39 | -------------------------------------------------------------------------------- /src/service/passport/index.ts: -------------------------------------------------------------------------------- 1 | import passport, { type PassportStatic } from 'passport'; 2 | import * as local from './local'; 3 | import * as activeDirectory from './activeDirectory'; 4 | import * as oidc from './oidc'; 5 | import * as config from '../../config'; 6 | import { AuthenticationElement } from '../../config/generated/config'; 7 | 8 | type StrategyModule = { 9 | configure: (passport: PassportStatic) => Promise; 10 | createDefaultAdmin?: () => Promise; 11 | type: string; 12 | }; 13 | 14 | export const authStrategies: Record = { 15 | local, 16 | activedirectory: activeDirectory, 17 | openidconnect: oidc, 18 | }; 19 | 20 | export const configure = async (): Promise => { 21 | passport.initialize(); 22 | 23 | const authMethods: AuthenticationElement[] = config.getAuthMethods(); 24 | 25 | for (const auth of authMethods) { 26 | const strategy = authStrategies[auth.type.toLowerCase()]; 27 | if (strategy && typeof strategy.configure === 'function') { 28 | await strategy.configure(passport); 29 | } 30 | } 31 | 32 | if (authMethods.some((auth) => auth.type.toLowerCase() === 'local')) { 33 | await local.createDefaultAdmin?.(); 34 | } 35 | 36 | return passport; 37 | }; 38 | 39 | export const getPassport = (): PassportStatic => passport; 40 | -------------------------------------------------------------------------------- /src/proxy/processors/types.ts: -------------------------------------------------------------------------------- 1 | import { Question } from '../../config/generated/config'; 2 | import { Action } from '../actions'; 3 | 4 | export interface Processor { 5 | exec(req: any, action: Action): Promise; 6 | metadata: ProcessorMetadata; 7 | } 8 | 9 | export interface ProcessorMetadata { 10 | displayName: string; 11 | } 12 | 13 | export type Attestation = { 14 | reviewer: { 15 | username: string; 16 | gitAccount: string; 17 | }; 18 | timestamp: string | Date; 19 | questions: Question[]; 20 | }; 21 | 22 | export type CommitContent = { 23 | item: number; 24 | type: number; 25 | typeName: string; 26 | size: number; 27 | baseSha: string | null; 28 | baseOffset: number | null; 29 | content: string; 30 | }; 31 | 32 | export type PersonLine = { 33 | name: string; 34 | email: string; 35 | timestamp: string; 36 | }; 37 | 38 | export type CommitHeader = { 39 | tree: string; 40 | parents: string[]; 41 | author: PersonLine; 42 | committer: PersonLine; 43 | }; 44 | 45 | export type CommitData = { 46 | tree: string; 47 | parent: string; 48 | author: string; 49 | committer: string; 50 | authorEmail: string; 51 | committerEmail: string; 52 | commitTimestamp: string; 53 | message: string; 54 | }; 55 | 56 | export type PackMeta = { 57 | sig: string; 58 | version: number; 59 | entries: number; 60 | }; 61 | -------------------------------------------------------------------------------- /.github/workflows/npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | on: 3 | release: 4 | types: [published] 5 | permissions: 6 | contents: read 7 | id-token: write 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Harden Runner 14 | uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 15 | with: 16 | egress-policy: audit 17 | 18 | - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 19 | # Setup .npmrc file to publish to npm 20 | - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 21 | with: 22 | node-version: '22.x' 23 | registry-url: 'https://registry.npmjs.org' 24 | - run: npm ci 25 | - run: npm run build 26 | env: 27 | IS_PUBLISHING: 'YES' 28 | 29 | - name: Check if pre-release and publish to NPM 30 | run: | 31 | VERSION=$(node -p "require('./package.json').version") 32 | if [[ "$VERSION" == *"-"* ]]; then 33 | echo "Publishing pre-release: $VERSION" 34 | npm publish --provenance --access=public --tag rc 35 | else 36 | echo "Publishing stable release: $VERSION" 37 | npm publish --provenance --access=public 38 | fi 39 | env: 40 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 41 | -------------------------------------------------------------------------------- /src/ui/services/config.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { API_BASE } from '../apiBase'; 3 | import { QuestionFormData } from '../types'; 4 | import { UIRouteAuth } from '../../config/generated/config'; 5 | 6 | const API_V1_BASE = `${API_BASE}/api/v1`; 7 | 8 | const setAttestationConfigData = async (setData: (data: QuestionFormData[]) => void) => { 9 | const url = new URL(`${API_V1_BASE}/config/attestation`); 10 | await axios(url.toString()).then((response) => { 11 | setData(response.data.questions); 12 | }); 13 | }; 14 | 15 | const setURLShortenerData = async (setData: (data: string) => void) => { 16 | const url = new URL(`${API_V1_BASE}/config/urlShortener`); 17 | await axios(url.toString()).then((response) => { 18 | setData(response.data); 19 | }); 20 | }; 21 | 22 | const setEmailContactData = async (setData: (data: string) => void) => { 23 | const url = new URL(`${API_V1_BASE}/config/contactEmail`); 24 | await axios(url.toString()).then((response) => { 25 | setData(response.data); 26 | }); 27 | }; 28 | 29 | const setUIRouteAuthData = async (setData: (data: UIRouteAuth) => void) => { 30 | const url = new URL(`${API_V1_BASE}/config/uiRouteAuth`); 31 | await axios(url.toString()).then((response) => { 32 | setData(response.data); 33 | }); 34 | }; 35 | 36 | export { setAttestationConfigData, setURLShortenerData, setEmailContactData, setUIRouteAuthData }; 37 | -------------------------------------------------------------------------------- /src/ui/views/Extras/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import Card from '../../components/Card/Card'; 4 | import CardBody from '../../components/Card/CardBody'; 5 | import GridContainer from '../../components/Grid/GridContainer'; 6 | import GridItem from '../../components/Grid/GridItem'; 7 | import { Button } from '@material-ui/core'; 8 | import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'; 9 | 10 | const NotFound = () => { 11 | const navigate = useNavigate(); 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 |

404 - Page Not Found

20 |

The page you are looking for does not exist. It may have been moved or deleted.

21 | 29 |
30 |
31 |
32 |
33 | ); 34 | }; 35 | 36 | export default NotFound; 37 | -------------------------------------------------------------------------------- /cypress/e2e/login.cy.js: -------------------------------------------------------------------------------- 1 | describe('Login page', () => { 2 | beforeEach(() => { 3 | cy.visit('/login'); 4 | }); 5 | 6 | it('should have git proxy logo', () => { 7 | cy.get('[data-test="git-proxy-logo"]').should('exist'); 8 | }); 9 | 10 | it('should have username input', () => { 11 | cy.get('[data-test="username"]').should('exist'); 12 | }); 13 | 14 | it('should have passsword input', () => { 15 | cy.get('[data-test="password"]').should('exist'); 16 | }); 17 | 18 | it('should have login button', () => { 19 | cy.get('[data-test="login"]').should('exist'); 20 | }); 21 | 22 | it('should redirect to repo list on valid login', () => { 23 | cy.intercept('GET', '**/api/auth/profile').as('getUser'); 24 | 25 | cy.get('[data-test="username"]').type('admin'); 26 | cy.get('[data-test="password"]').type('admin'); 27 | cy.get('[data-test="login"]').click(); 28 | 29 | cy.wait('@getUser'); 30 | 31 | cy.url().should('include', '/dashboard/repo'); 32 | }); 33 | 34 | it('should show an error snackbar on invalid login', () => { 35 | cy.get('[data-test="username"]').type('wronguser'); 36 | cy.get('[data-test="password"]').type('wrongpass'); 37 | cy.get('[data-test="login"]').click(); 38 | 39 | cy.get('.MuiSnackbarContent-message') 40 | .should('be.visible') 41 | .and('contain', 'You entered an invalid username or password...'); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/db/schemas/license/license.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import { License } from '@/db/collections'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | 5 | // these tests require a mongodb instance 6 | const describeDB = process.env.MONGO_URI?.startsWith('mongodb') ? describe : describe.skip; 7 | 8 | describeDB('license', () => { 9 | it('can insert a basic license', async () => { 10 | const _id = uuidv4(); 11 | await License.create({ 12 | _id, 13 | name: 'hello', 14 | }); 15 | 16 | const insertedLicense = await License.findOne({ _id: _id }); 17 | expect(insertedLicense).not.toBeNull(); 18 | expect(insertedLicense!.name).toBe('hello'); 19 | }); 20 | 21 | it('can insert a complex license', async () => { 22 | const _id = uuidv4(); 23 | await License.create({ 24 | _id, 25 | name: 'complex', 26 | spdxID: 'sample', 27 | chooseALicenseInfo: { 28 | permissions: { 29 | commercialUse: true, 30 | }, 31 | conditions: { 32 | networkUseDisclose: false, 33 | }, 34 | }, 35 | }); 36 | 37 | const insertedLicense = await License.findOne({ _id: _id }); 38 | expect(insertedLicense).not.toBeNull(); 39 | expect(insertedLicense!.name).toBe('complex'); 40 | expect(insertedLicense!.chooseALicenseInfo?.permissions?.commercialUse).toBe(true); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/proxy/processors/push-action/checkEmptyBranch.ts: -------------------------------------------------------------------------------- 1 | import { Action, Step } from '../../actions'; 2 | import simpleGit from 'simple-git'; 3 | import { EMPTY_COMMIT_HASH } from '../constants'; 4 | 5 | const isEmptyBranch = async (action: Action) => { 6 | if (action.commitFrom === EMPTY_COMMIT_HASH) { 7 | try { 8 | const git = simpleGit(`${action.proxyGitPath}/${action.repoName}`); 9 | 10 | const type = await git.raw(['cat-file', '-t', action.commitTo || '']); 11 | return type.trim() === 'commit'; 12 | } catch (err) { 13 | console.log(`Commit ${action.commitTo} not found: ${err}`); 14 | } 15 | } 16 | 17 | return false; 18 | }; 19 | 20 | const exec = async (req: any, action: Action): Promise => { 21 | const step = new Step('checkEmptyBranch'); 22 | 23 | if (action.commitData && action.commitData.length > 0) { 24 | return action; 25 | } 26 | 27 | if (await isEmptyBranch(action)) { 28 | step.setError('Push blocked: Empty branch. Please make a commit before pushing a new branch.'); 29 | action.addStep(step); 30 | step.error = true; 31 | return action; 32 | } else { 33 | step.setError( 34 | 'Push blocked: Commit data not found. Please contact an administrator for support.', 35 | ); 36 | action.addStep(step); 37 | step.error = true; 38 | return action; 39 | } 40 | }; 41 | 42 | exec.displayName = 'checkEmptyBranch.exec'; 43 | 44 | export { exec }; 45 | -------------------------------------------------------------------------------- /.github/workflows/experimental-inventory-cli-publish.yml: -------------------------------------------------------------------------------- 1 | name: experimental-inventory-cli - Publish to NPM 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: 'release version without v prefix' 7 | required: true 8 | type: string 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Harden Runner 17 | uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 18 | with: 19 | egress-policy: audit 20 | 21 | - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 22 | 23 | # Setup .npmrc file to publish to npm 24 | - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 25 | with: 26 | node-version: '22.x' 27 | registry-url: 'https://registry.npmjs.org' 28 | 29 | - name: check version matches input 30 | run: | 31 | grep "\"version\": \"${{ github.event.inputs.version }}\"," package.json 32 | working-directory: ./experimental/li-cli 33 | 34 | - run: npm ci 35 | working-directory: ./experimental/li-cli 36 | 37 | - run: npm run build 38 | working-directory: ./experimental/li-cli 39 | 40 | - run: npm publish --access=public 41 | working-directory: ./experimental/li-cli 42 | env: 43 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | -------------------------------------------------------------------------------- /src/proxy/processors/push-action/index.ts: -------------------------------------------------------------------------------- 1 | import { exec as parsePush } from './parsePush'; 2 | import { exec as preReceive } from './preReceive'; 3 | import { exec as checkRepoInAuthorisedList } from './checkRepoInAuthorisedList'; 4 | import { exec as audit } from './audit'; 5 | import { exec as pullRemote } from './pullRemote'; 6 | import { exec as writePack } from './writePack'; 7 | import { exec as getDiff } from './getDiff'; 8 | import { exec as checkHiddenCommits } from './checkHiddenCommits'; 9 | import { exec as gitleaks } from './gitleaks'; 10 | import { exec as scanDiff } from './scanDiff'; 11 | import { exec as blockForAuth } from './blockForAuth'; 12 | import { exec as checkIfWaitingAuth } from './checkIfWaitingAuth'; 13 | import { exec as checkCommitMessages } from './checkCommitMessages'; 14 | import { exec as checkAuthorEmails } from './checkAuthorEmails'; 15 | import { exec as checkUserPushPermission } from './checkUserPushPermission'; 16 | import { exec as clearBareClone } from './clearBareClone'; 17 | import { exec as checkEmptyBranch } from './checkEmptyBranch'; 18 | 19 | export { 20 | parsePush, 21 | preReceive, 22 | checkRepoInAuthorisedList, 23 | audit, 24 | pullRemote, 25 | writePack, 26 | getDiff, 27 | checkHiddenCommits, 28 | gitleaks, 29 | scanDiff, 30 | blockForAuth, 31 | checkIfWaitingAuth, 32 | checkCommitMessages, 33 | checkAuthorEmails, 34 | checkUserPushPermission, 35 | clearBareClone, 36 | checkEmptyBranch, 37 | }; 38 | -------------------------------------------------------------------------------- /website/docs/installation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | description: How to install GitProxy in your environment 4 | --- 5 | 6 | ### Install via [npm](https://www.npmjs.com/package/@finos/git-proxy) 7 | 8 | To install GitProxy, you must first install [Node.js](https://nodejs.org/en/download). Then, use the [npm package manager](https://www.npmjs.com/): 9 | 10 | ```bash 11 | npm install -g @finos/git-proxy 12 | ``` 13 | 14 | To install the GitProxy Command Line Interface (CLI), run: 15 | 16 | ```bash 17 | npm install -g @finos/git-proxy-cli 18 | ``` 19 | 20 | ### Install a specific version 21 | 22 | To install a specific version of GitProxy, append the version to the end of the install command: 23 | 24 | ```bash 25 | npm install -g @finos/git-proxy@latest 26 | ``` 27 | 28 | To install a specific version of the GitProxy CLI, append the version to the end of the install command: 29 | 30 | ```bash 31 | npm install -g @finos/git-proxy-cli@1.0.0 32 | ``` 33 | 34 | ### Install a local checkout 35 | 36 | To test a personal fork, or the latest version of the main branch: 37 | 38 | ```bash 39 | git clone git@github.com:finos/git-proxy.git 40 | cd git-proxy 41 | npm pack 42 | npm i -g finos-git-proxy-.tar.gz 43 | git-proxy --version 44 | ``` 45 | 46 | To make sure that the `git-proxy` command is using your checked out version, update the `version` in `package.json` before running `npm pack`, then verify that the same version is returned when running `git-proxy --version`. 47 | -------------------------------------------------------------------------------- /src/config/types.ts: -------------------------------------------------------------------------------- 1 | import { GitProxyConfig } from './generated/config'; 2 | 3 | export type ServerConfig = { 4 | GIT_PROXY_SERVER_PORT: string | number; 5 | GIT_PROXY_HTTPS_SERVER_PORT: string | number; 6 | GIT_PROXY_UI_HOST: string; 7 | GIT_PROXY_UI_PORT: string | number; 8 | GIT_PROXY_COOKIE_SECRET: string | undefined; 9 | GIT_PROXY_MONGO_CONNECTION_STRING: string; 10 | }; 11 | 12 | interface GitAuth { 13 | type: 'ssh'; 14 | privateKeyPath: string; 15 | } 16 | 17 | interface HttpAuth { 18 | type: 'bearer'; 19 | token: string; 20 | } 21 | 22 | interface BaseSource { 23 | type: 'file' | 'http' | 'git'; 24 | enabled: boolean; 25 | } 26 | 27 | export interface FileSource extends BaseSource { 28 | type: 'file'; 29 | path: string; 30 | } 31 | 32 | export interface HttpSource extends BaseSource { 33 | type: 'http'; 34 | url: string; 35 | headers?: Record; 36 | auth?: HttpAuth; 37 | } 38 | 39 | export interface GitSource extends BaseSource { 40 | type: 'git'; 41 | repository: string; 42 | branch?: string; 43 | path: string; 44 | auth?: GitAuth; 45 | } 46 | 47 | export type ConfigurationSource = FileSource | HttpSource | GitSource; 48 | 49 | interface ConfigurationSources { 50 | enabled: boolean; 51 | sources: ConfigurationSource[]; 52 | reloadIntervalSeconds: number; 53 | merge?: boolean; 54 | } 55 | 56 | export interface Configuration extends GitProxyConfig { 57 | configurationSources?: ConfigurationSources; 58 | } 59 | -------------------------------------------------------------------------------- /src/ui/assets/jss/material-dashboard-react/components/tasksStyle.js: -------------------------------------------------------------------------------- 1 | import { 2 | defaultFont, 3 | primaryColor, 4 | dangerColor, 5 | grayColor, 6 | } from '../../material-dashboard-react.js'; 7 | import tooltipStyle from '..t/tooltipStyle.js'; 8 | import checkboxAdnRadioStyle from '../checkboxAdnRadioStyle.js'; 9 | const tasksStyle = { 10 | ...tooltipStyle, 11 | ...checkboxAdnRadioStyle, 12 | table: { 13 | marginBottom: '0', 14 | overflow: 'visible', 15 | }, 16 | tableRow: { 17 | position: 'relative', 18 | borderBottom: '1px solid ' + grayColor[5], 19 | }, 20 | tableActions: { 21 | display: 'flex', 22 | border: 'none', 23 | padding: '12px 8px !important', 24 | verticalAlign: 'middle', 25 | }, 26 | tableCell: { 27 | ...defaultFont, 28 | padding: '8px', 29 | verticalAlign: 'middle', 30 | border: 'none', 31 | lineHeight: '1.42857143', 32 | fontSize: '14px', 33 | }, 34 | tableCellRTL: { 35 | textAlign: 'right', 36 | }, 37 | tableActionButton: { 38 | width: '27px', 39 | height: '27px', 40 | padding: '0', 41 | }, 42 | tableActionButtonIcon: { 43 | width: '17px', 44 | height: '17px', 45 | }, 46 | edit: { 47 | backgroundColor: 'transparent', 48 | color: primaryColor[0], 49 | boxShadow: 'none', 50 | }, 51 | close: { 52 | backgroundColor: 'transparent', 53 | color: dangerColor[0], 54 | boxShadow: 'none', 55 | }, 56 | }; 57 | export default tasksStyle; 58 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Code Cleanliness 2 | 3 | on: [pull_request] 4 | 5 | env: 6 | NODE_VERSION: 20 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | linting: 13 | name: Linting 14 | runs-on: ubuntu-latest 15 | steps: # list of steps 16 | - name: Harden Runner 17 | uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2 18 | with: 19 | egress-policy: audit 20 | 21 | - name: Install NodeJS 22 | uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 23 | with: 24 | node-version: ${{ env.NODE_VERSION }} 25 | 26 | - name: Code Checkout 27 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 28 | with: 29 | fetch-depth: 0 30 | 31 | - name: Install Dependencies 32 | run: npm install --workspaces 33 | 34 | - name: Code Linting 35 | run: | 36 | npm run lint 37 | npm run lint --workspaces --if-present 38 | 39 | - name: Check formatting 40 | run: npm run format:check 41 | 42 | - name: Check generated config types are up-to-date 43 | run: | 44 | npm run generate-config-types 45 | if ! git diff --exit-code src/config/generated/config.ts; then 46 | echo "Generated config types are out of date. Run 'npm run generate-config-types' locally and commit the changes." 47 | exit 1 48 | fi 49 | -------------------------------------------------------------------------------- /.github/workflows/pr-lint.yml: -------------------------------------------------------------------------------- 1 | ## Reference: https://github.com/amannn/action-semantic-pull-request 2 | --- 3 | name: 'PR' 4 | 5 | on: 6 | pull_request_target: 7 | types: 8 | - opened 9 | - reopened 10 | - edited 11 | - synchronize 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | pr_title: 18 | permissions: 19 | pull-requests: write 20 | statuses: write 21 | name: Validate & Label PR 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Harden Runner 25 | uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 26 | with: 27 | egress-policy: audit 28 | 29 | - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | with: 33 | # Configure which types are allowed (newline-delimited). 34 | # From: https://github.com/commitizen/conventional-commit-types/blob/master/index.json 35 | # listing all below 36 | types: | 37 | chore 38 | ci 39 | docs 40 | feat 41 | fix 42 | perf 43 | refactor 44 | revert 45 | test 46 | break 47 | - uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | -------------------------------------------------------------------------------- /src/proxy/processors/push-action/writePack.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { Action, Step } from '../../actions'; 3 | import { spawnSync } from 'child_process'; 4 | import fs from 'fs'; 5 | 6 | const exec = async (req: any, action: Action) => { 7 | const step = new Step('writePack'); 8 | try { 9 | if (!action.proxyGitPath || !action.repoName) { 10 | throw new Error('proxyGitPath and repoName must be defined'); 11 | } 12 | const repoPath = path.join(action.proxyGitPath, action.repoName); 13 | 14 | const packDir = path.join(repoPath, '.git', 'objects', 'pack'); 15 | 16 | spawnSync('git', ['config', 'receive.unpackLimit', '0'], { 17 | cwd: repoPath, 18 | encoding: 'utf-8', 19 | }); 20 | const before = new Set(fs.readdirSync(packDir).filter((f) => f.endsWith('.idx'))); 21 | const content = spawnSync('git', ['receive-pack', action.repoName], { 22 | cwd: action.proxyGitPath, 23 | input: req.body, 24 | }); 25 | const newIdxFiles = [ 26 | ...new Set(fs.readdirSync(packDir).filter((f) => f.endsWith('.idx') && !before.has(f))), 27 | ]; 28 | action.newIdxFiles = newIdxFiles; 29 | step.log(`new idx files: ${newIdxFiles}`); 30 | 31 | step.setContent(content); 32 | } catch (e: any) { 33 | step.setError(e.toString('utf-8')); 34 | throw e; 35 | } finally { 36 | action.addStep(step); 37 | } 38 | return action; 39 | }; 40 | 41 | exec.displayName = 'writePack.exec'; 42 | 43 | export { exec }; 44 | -------------------------------------------------------------------------------- /.github/workflows/experimental-inventory-publish.yml: -------------------------------------------------------------------------------- 1 | name: experimental-inventory - Publish to NPM 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: 'release version without v prefix' 7 | required: true 8 | type: string 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Harden Runner 17 | uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 18 | with: 19 | egress-policy: audit 20 | 21 | - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 22 | 23 | # Setup .npmrc file to publish to npm 24 | - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 25 | with: 26 | node-version: '22.x' 27 | registry-url: 'https://registry.npmjs.org' 28 | 29 | - name: check version matches input 30 | run: | 31 | grep "\"version\": \"${{ github.event.inputs.version }}\"," package.json 32 | working-directory: ./experimental/license-inventory 33 | 34 | - run: npm ci 35 | working-directory: ./experimental/license-inventory 36 | 37 | - run: npm run build 38 | working-directory: ./experimental/license-inventory 39 | 40 | - run: npm publish --access=public 41 | working-directory: ./experimental/license-inventory 42 | env: 43 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | -------------------------------------------------------------------------------- /src/ui/views/Extras/NotAuthorized.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import Card from '../../components/Card/Card'; 4 | import CardBody from '../../components/Card/CardBody'; 5 | import GridContainer from '../../components/Grid/GridContainer'; 6 | import GridItem from '../../components/Grid/GridItem'; 7 | import { Button } from '@material-ui/core'; 8 | import LockIcon from '@material-ui/icons/Lock'; 9 | 10 | const NotAuthorized = () => { 11 | const navigate = useNavigate(); 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 |

403 - Not Authorized

20 |

21 | You do not have permission to access this page. Contact your administrator for more 22 | information, or try logging in with a different account. 23 |

24 | 32 |
33 |
34 |
35 |
36 | ); 37 | }; 38 | 39 | export default NotAuthorized; 40 | -------------------------------------------------------------------------------- /test/proxyURL.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, afterEach, expect, vi } from 'vitest'; 2 | import request from 'supertest'; 3 | import express from 'express'; 4 | 5 | import { getProxyURL } from '../src/service/urls'; 6 | import * as config from '../src/config'; 7 | 8 | const genSimpleServer = () => { 9 | const app = express(); 10 | app.get('/', (req, res) => { 11 | res.type('html'); 12 | res.send(getProxyURL(req)); 13 | }); 14 | return app; 15 | }; 16 | 17 | describe('proxyURL', () => { 18 | afterEach(() => { 19 | vi.restoreAllMocks(); 20 | }); 21 | 22 | it('pulls the request path with no override', async () => { 23 | const app = genSimpleServer(); 24 | const res = await request(app).get('/'); 25 | 26 | expect(res.status).toBe(200); 27 | 28 | // request url without trailing slash 29 | const reqURL = res.request.url.slice(0, -1); 30 | expect(res.text).toBe(reqURL); 31 | expect(res.text).toMatch(/https?:\/\/127.0.0.1:\d+/); 32 | }); 33 | 34 | it('can override providing a proxy value', async () => { 35 | const proxyURL = 'https://amazing-proxy.path.local'; 36 | 37 | // stub getDomains 38 | const spy = vi.spyOn(config, 'getDomains').mockReturnValue({ proxy: proxyURL }); 39 | 40 | const app = genSimpleServer(); 41 | const res = await request(app).get('/'); 42 | 43 | expect(res.status).toBe(200); 44 | 45 | // the stub worked 46 | expect(spy).toHaveBeenCalledTimes(1); 47 | 48 | expect(res.text).toBe(proxyURL); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /website/sidebars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mainSidebar: [ 3 | 'index', 4 | { 5 | type: 'category', 6 | label: 'Quickstart', 7 | link: { 8 | type: 'generated-index', 9 | title: 'Quickstart', 10 | slug: '/category/quickstart', 11 | keywords: ['get started', 'quickstart'], 12 | image: '/img/github-mark.png', 13 | }, 14 | collapsible: true, 15 | collapsed: false, 16 | items: ['quickstart/intercept', 'quickstart/approve'], 17 | }, 18 | 'installation', 19 | 'usage', 20 | { 21 | type: 'category', 22 | label: 'Configuration', 23 | link: { 24 | type: 'generated-index', 25 | title: 'Configuration', 26 | slug: '/category/configuration', 27 | keywords: ['config', 'configuration'], 28 | image: '/img/github-mark.png', 29 | }, 30 | collapsible: true, 31 | collapsed: false, 32 | items: ['configuration/overview', 'configuration/reference', 'configuration/pre-receive'], 33 | }, 34 | { 35 | type: 'category', 36 | label: 'Development', 37 | link: { 38 | type: 'generated-index', 39 | title: 'Development', 40 | slug: '/category/development', 41 | keywords: ['dev', 'development'], 42 | image: '/img/github-mark.png', 43 | }, 44 | collapsible: true, 45 | collapsed: false, 46 | items: ['development/contributing', 'development/plugins', 'development/testing'], 47 | }, 48 | ], 49 | }; 50 | -------------------------------------------------------------------------------- /experimental/li-cli/src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import yargs from 'yargs'; 4 | import { hideBin } from 'yargs/helpers'; 5 | import addLicenseCMD from '@/cmds/add-license'; 6 | import process from 'node:process'; 7 | 8 | yargs(hideBin(process.argv)) 9 | .option('li-url', { 10 | type: 'string', 11 | describe: 'The url of the license inventory instance', 12 | }) 13 | .command( 14 | 'add-license [SPDXID]', 15 | '', 16 | (yargs) => 17 | yargs 18 | .positional('SPDXID', { 19 | type: 'string', 20 | describe: 'ID of license', 21 | }) 22 | .option('require-cal', { 23 | type: 'boolean', 24 | default: false, 25 | describe: 'require successful collection of info from Choose A License', 26 | }) 27 | .option('allow-deprecated', { 28 | type: 'boolean', 29 | default: false, 30 | describe: 'allow for adding depricated licenses', 31 | }) 32 | .demandOption('li-url') 33 | .strict(), 34 | async (argv) => { 35 | try { 36 | await addLicenseCMD(argv['li-url'], { 37 | spdxID: argv.SPDXID, 38 | requireCal: argv['require-cal'], 39 | allowDeprecated: argv['allow-deprecated'], 40 | }); 41 | } catch (e) { 42 | process.exit(1); 43 | } 44 | }, 45 | ) 46 | .option('verbose', { 47 | alias: 'v', 48 | type: 'boolean', 49 | description: 'Run with verbose logging', 50 | }) 51 | .demandCommand() 52 | .help() 53 | .parse(); 54 | -------------------------------------------------------------------------------- /src/ui/assets/jss/material-dashboard-react/components/footerStyle.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '@material-ui/core/styles'; 2 | import { createStyles } from '@material-ui/styles'; 3 | import { defaultFont, container, primaryColor, grayColor } from '../../material-dashboard-react'; 4 | 5 | const footerStyle = (theme: Theme) => 6 | createStyles({ 7 | block: { 8 | color: 'inherit', 9 | padding: '15px', 10 | textTransform: 'uppercase', 11 | borderRadius: '3px', 12 | textDecoration: 'none', 13 | position: 'relative', 14 | display: 'block', 15 | ...defaultFont, 16 | fontWeight: 500, 17 | fontSize: '12px', 18 | }, 19 | left: { 20 | float: 'left !important' as 'left', 21 | display: 'block', 22 | }, 23 | right: { 24 | padding: '15px 0', 25 | margin: '0', 26 | fontSize: '14px', 27 | float: 'right !important' as 'right', 28 | }, 29 | footer: { 30 | bottom: '0', 31 | borderTop: `1px solid ${grayColor[11]}`, 32 | padding: '15px 0', 33 | ...defaultFont, 34 | }, 35 | container: { 36 | ...container, 37 | }, 38 | a: { 39 | color: primaryColor[0], 40 | textDecoration: 'none', 41 | backgroundColor: 'transparent', 42 | }, 43 | list: { 44 | marginBottom: '0', 45 | padding: '0', 46 | marginTop: '0', 47 | }, 48 | inlineBlock: { 49 | display: 'inline-block', 50 | padding: '0px', 51 | width: 'auto', 52 | }, 53 | }); 54 | 55 | export default footerStyle; 56 | -------------------------------------------------------------------------------- /experimental/li-cli/src/lib/spdx.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | import localSPDX from './licenses.json'; 3 | 4 | const licenseSchema = z.object({ 5 | isDeprecatedLicenseId: z.boolean(), 6 | detailsUrl: z.string().url(), 7 | name: z.string(), 8 | licenseId: z.string(), 9 | }); 10 | 11 | export type License = z.infer; 12 | export type LicensesMap = Map; 13 | 14 | const licensesSchema = z.object({ 15 | licenseListVersion: z.string(), 16 | licenses: licenseSchema.array(), 17 | }); 18 | export type Licenses = z.infer; 19 | 20 | export const getLicenseList = async (allowDeprecated: boolean) => { 21 | let licenses: Licenses | undefined = undefined; 22 | try { 23 | // https://spdx.org/licenses/licenses.json 24 | const req = await fetch('https://spdx.org/licenses/licenses.json'); 25 | const data = await req.json(); 26 | const { data: parsed, error } = licensesSchema.safeParse(data); 27 | if (error) { 28 | throw new Error("couldn't get license list", { cause: error }); 29 | } 30 | licenses = parsed; 31 | } catch (e: unknown) { 32 | console.warn('failed to fetch upstream licenses, falling back to offline copy'); 33 | licenses = localSPDX; 34 | } 35 | 36 | const licenseMap: LicensesMap = new Map(); 37 | (licenses ?? ({} as Licenses)).licenses.forEach((license) => { 38 | if (!allowDeprecated && license.isDeprecatedLicenseId) { 39 | return; 40 | } 41 | licenseMap.set(license.licenseId.toLowerCase(), license); 42 | }); 43 | 44 | return licenseMap; 45 | }; 46 | -------------------------------------------------------------------------------- /test/processors/clearBareClone.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, afterEach } from 'vitest'; 2 | import fs from 'fs'; 3 | import { exec as clearBareClone } from '../../src/proxy/processors/push-action/clearBareClone'; 4 | import { exec as pullRemote } from '../../src/proxy/processors/push-action/pullRemote'; 5 | import { Action } from '../../src/proxy/actions/Action'; 6 | 7 | const actionId = '123__456'; 8 | const timestamp = Date.now(); 9 | 10 | describe('clear bare and local clones', () => { 11 | it('pull remote generates a local .remote folder', async () => { 12 | const action = new Action(actionId, 'type', 'get', timestamp, 'finos/git-proxy.git'); 13 | action.url = 'https://github.com/finos/git-proxy.git'; 14 | const authorization = `Basic ${Buffer.from('JamieSlome:test').toString('base64')}`; 15 | 16 | await pullRemote( 17 | { 18 | headers: { 19 | authorization, 20 | }, 21 | }, 22 | action, 23 | ); 24 | 25 | expect(fs.existsSync(`./.remote/${actionId}`)).toBe(true); 26 | }, 20000); 27 | 28 | it('clear bare clone function purges .remote folder and specific clone folder', async () => { 29 | const action = new Action(actionId, 'type', 'get', timestamp, 'finos/git-proxy.git'); 30 | await clearBareClone(null, action); 31 | 32 | expect(fs.existsSync(`./.remote`)).toBe(false); 33 | expect(fs.existsSync(`./.remote/${actionId}`)).toBe(false); 34 | }); 35 | 36 | afterEach(() => { 37 | if (fs.existsSync(`./.remote`)) { 38 | fs.rmSync(`./.remote`, { recursive: true }); 39 | } 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /.github/workflows/experimental-inventory-ci.yml: -------------------------------------------------------------------------------- 1 | name: CI - experimental - inventory 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - experimental/license-inventory/** 8 | pull_request: 9 | branches: [main] 10 | paths: 11 | - experimental/license-inventory/** 12 | 13 | permissions: 14 | pull-requests: write 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | strategy: 21 | matrix: 22 | node-version: [20.x] 23 | mongodb-version: [4.4] 24 | 25 | steps: 26 | - name: Harden Runner 27 | uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 28 | with: 29 | egress-policy: audit 30 | 31 | - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 32 | with: 33 | fetch-depth: 0 34 | 35 | - name: Use Node.js ${{ matrix.node-version }} 36 | uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 37 | with: 38 | node-version: ${{ matrix.node-version }} 39 | 40 | - name: Start MongoDB 41 | uses: supercharge/mongodb-github-action@315db7fe45ac2880b7758f1933e6e5d59afd5e94 # 1.12.1 42 | with: 43 | mongodb-version: ${{ matrix.mongodb-version }} 44 | 45 | - name: Install dependencies 46 | working-directory: ./experimental/license-inventory 47 | run: npm ci 48 | 49 | - name: Test 50 | working-directory: ./experimental/license-inventory 51 | run: | 52 | MONGO_URI="mongodb://localhost:27017/inventory" npm run test 53 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: 'CodeQL' 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | branches: ['main'] 8 | schedule: 9 | - cron: '25 10 * * 1' 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | analyze: 16 | name: Analyze 17 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 18 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 19 | permissions: 20 | security-events: write 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | language: ['javascript-typescript'] 26 | 27 | steps: 28 | - name: Harden Runner 29 | uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # ratchet:step-security/harden-runner@v2 30 | with: 31 | egress-policy: audit 32 | 33 | - name: Checkout repository 34 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # ratchet:actions/checkout@v6 35 | 36 | - name: Initialize CodeQL 37 | uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # ratchet:github/codeql-action/init@v4 38 | with: 39 | languages: ${{ matrix.language }} 40 | 41 | - name: Autobuild 42 | uses: github/codeql-action/autobuild@1b168cd39490f61582a9beae412bb7057a6b2c4e # ratchet:github/codeql-action/autobuild@v4 43 | 44 | - name: Perform CodeQL Analysis 45 | uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # ratchet:github/codeql-action/analyze@v4 46 | with: 47 | category: '/language:${{matrix.language}}' 48 | -------------------------------------------------------------------------------- /src/proxy/actions/Step.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | 3 | export interface StepData { 4 | id: string; 5 | stepName: string; 6 | content: any; 7 | error: boolean; 8 | errorMessage: string | null; 9 | blocked: boolean; 10 | blockedMessage: string | null; 11 | logs: string[]; 12 | } 13 | 14 | /** Class representing a Push Step. */ 15 | class Step implements StepData { 16 | id: string; 17 | stepName: string; 18 | content: any; 19 | error: boolean; 20 | errorMessage: string | null; 21 | blocked: boolean; 22 | blockedMessage: string | null; 23 | logs: string[] = []; 24 | 25 | constructor( 26 | stepName: string, 27 | error: boolean = false, 28 | errorMessage: string | null = null, 29 | blocked: boolean = false, 30 | blockedMessage: string | null = null, 31 | content: any = null, 32 | ) { 33 | this.id = uuidv4(); 34 | this.stepName = stepName; 35 | this.content = content; 36 | this.error = error; 37 | this.errorMessage = errorMessage; 38 | this.blocked = blocked; 39 | this.blockedMessage = blockedMessage; 40 | } 41 | 42 | setError(message: string): void { 43 | this.error = true; 44 | this.errorMessage = message; 45 | this.log(message); 46 | } 47 | 48 | setContent(content: any): void { 49 | this.content = content; 50 | } 51 | 52 | setAsyncBlock(message: string): void { 53 | this.blocked = true; 54 | this.blockedMessage = message; 55 | } 56 | 57 | log(message: string): void { 58 | const m = `${this.stepName} - ${message}`; 59 | this.logs.push(m); 60 | console.info(m); 61 | } 62 | } 63 | 64 | export { Step }; 65 | -------------------------------------------------------------------------------- /src/proxy/processors/push-action/getDiff.ts: -------------------------------------------------------------------------------- 1 | import { Action, Step } from '../../actions'; 2 | import simpleGit from 'simple-git'; 3 | 4 | import { EMPTY_COMMIT_HASH } from '../constants'; 5 | 6 | const exec = async (req: any, action: Action): Promise => { 7 | const step = new Step('diff'); 8 | 9 | try { 10 | const path = `${action.proxyGitPath}/${action.repoName}`; 11 | const git = simpleGit(path); 12 | // https://stackoverflow.com/questions/40883798/how-to-get-git-diff-of-the-first-commit 13 | let commitFrom = `4b825dc642cb6eb9a060e54bf8d69288fbee4904`; 14 | 15 | if (!action.commitData || action.commitData.length === 0) { 16 | step.error = true; 17 | step.log('No commitData found'); 18 | step.setError('Your push has been blocked because no commit data was found.'); 19 | action.addStep(step); 20 | return action; 21 | } 22 | 23 | if (action.commitFrom === EMPTY_COMMIT_HASH) { 24 | if (action.commitData[0].parent !== EMPTY_COMMIT_HASH) { 25 | commitFrom = `${action.commitData[action.commitData.length - 1].parent}`; 26 | } 27 | } else { 28 | commitFrom = `${action.commitFrom}`; 29 | } 30 | 31 | step.log(`Executing "git diff ${commitFrom} ${action.commitTo}" in ${path}`); 32 | const revisionRange = `${commitFrom}..${action.commitTo}`; 33 | const diff = await git.diff([revisionRange]); 34 | step.log(diff); 35 | step.setContent(diff); 36 | } catch (e: any) { 37 | step.setError(e.toString('utf-8')); 38 | } finally { 39 | action.addStep(step); 40 | } 41 | return action; 42 | }; 43 | 44 | exec.displayName = 'getDiff.exec'; 45 | 46 | export { exec }; 47 | -------------------------------------------------------------------------------- /experimental/li-cli/src/lib/inventory.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export type CalInfo = { 4 | permissions?: { 5 | commercialUse?: boolean; 6 | modifications?: boolean; 7 | distribution?: boolean; 8 | privateUse?: boolean; 9 | patentUse?: boolean; 10 | }; 11 | conditions?: { 12 | includeCopyright?: boolean; 13 | includeCopyrightSource?: boolean; 14 | documentChanges?: boolean; 15 | discloseSource?: boolean; 16 | networkUseDisclose?: boolean; 17 | sameLicense?: boolean; 18 | sameLicenseFile?: boolean; 19 | sameLicenseLibrary?: boolean; 20 | }; 21 | limitations?: { 22 | trademarkUse?: boolean; 23 | liability?: boolean; 24 | patentUse?: boolean; 25 | warranty?: boolean; 26 | }; 27 | }; 28 | 29 | export type InventoryLicense = { 30 | name: string; 31 | spdxID?: string; 32 | chooseALicenseInfo?: CalInfo; 33 | }; 34 | 35 | const pushLicenseSchema = z.object({ 36 | id: z.string().uuid(), 37 | }); 38 | 39 | export async function pushLicense(liURL: string, data: InventoryLicense): Promise { 40 | const path = '/api/v0/licenses/'; 41 | const res = await fetch(liURL + path, { 42 | method: 'POST', 43 | headers: { 44 | Accept: 'application/json', 45 | 'Content-Type': 'application/json', 46 | }, 47 | body: JSON.stringify(data), 48 | }); 49 | if (!res.ok) { 50 | throw new Error(await res.text()); 51 | } 52 | const resObj = await res.json(); 53 | const { data: resData, error } = pushLicenseSchema.safeParse(resObj); 54 | // TODO: account for already exists 55 | if (error) { 56 | throw new Error("couldn't process data", { cause: error }); 57 | } 58 | return resData.id; 59 | } 60 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name-template: 'Version $RESOLVED_VERSION' 3 | tag-template: 'v$RESOLVED_VERSION' 4 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 5 | template: | 6 | ### What's Changed 7 | 8 | $CHANGES 9 | 10 | --- 11 | 12 | *Full Changelog**: https://github.com/finos/git-proxy/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION 13 | 14 | categories: 15 | - title: '🚀 Features' 16 | labels: 17 | - 'enhancement' 18 | - 'feature' 19 | - title: '🐛 Bug Fixes' 20 | labels: 21 | - 'fix' 22 | - title: '🧰 Maintenance' 23 | labels: 24 | - 'infrastructure' 25 | - 'automation' 26 | - 'documentation' 27 | - 'dependencies' 28 | - 'maintenance' 29 | - 'revert' 30 | version-resolver: 31 | major: 32 | labels: 33 | - 'breaking' 34 | minor: 35 | labels: 36 | - 'enhancement' 37 | - 'feature' 38 | patch: 39 | labels: 40 | - 'fix' 41 | - 'documentation' 42 | - 'maintenance' 43 | default: patch 44 | autolabeler: 45 | - label: 'automation' 46 | title: 47 | - '/^(ci|perf|refactor|test).*/i' 48 | - label: 'enhancement' 49 | title: 50 | - '/^(style).*/i' 51 | - label: 'documentation' 52 | title: 53 | - '/^(docs).*/i' 54 | - label: 'feature' 55 | title: 56 | - '/^(feat).*/i' 57 | - label: 'breaking' 58 | title: 59 | - '/^(break).*/i' 60 | - label: 'fix' 61 | title: 62 | - '/^(fix).*/i' 63 | - label: 'infrastructure' 64 | title: 65 | - '/^(infrastructure).*/i' 66 | - label: 'maintenance' 67 | title: 68 | - '/^(chore|maintenance).*/i' 69 | - label: 'revert' 70 | title: 71 | - '/^(revert).*/i' 72 | -------------------------------------------------------------------------------- /test/ui/apiBase.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'; 2 | 3 | async function loadApiBase() { 4 | const path = '../../src/ui/apiBase.ts'; 5 | const modulePath = await import(path + '?update=' + Date.now()); // forces reload 6 | return modulePath; 7 | } 8 | 9 | describe('apiBase', () => { 10 | let originalEnv: string | undefined; 11 | const originalLocation = globalThis.location; 12 | 13 | beforeAll(() => { 14 | globalThis.location = { origin: 'https://lovely-git-proxy.com' } as any; 15 | }); 16 | 17 | afterAll(() => { 18 | globalThis.location = originalLocation; 19 | }); 20 | 21 | beforeEach(() => { 22 | originalEnv = process.env.VITE_API_URI; 23 | delete process.env.VITE_API_URI; 24 | }); 25 | 26 | afterEach(() => { 27 | if (typeof originalEnv === 'undefined') { 28 | delete process.env.VITE_API_URI; 29 | } else { 30 | process.env.VITE_API_URI = originalEnv; 31 | } 32 | }); 33 | 34 | it('uses the location origin when VITE_API_URI is not set', async () => { 35 | const { API_BASE } = await loadApiBase(); 36 | expect(API_BASE).toBe('https://lovely-git-proxy.com'); 37 | }); 38 | 39 | it('returns the exact value when no trailing slash', async () => { 40 | process.env.VITE_API_URI = 'https://example.com'; 41 | const { API_BASE } = await loadApiBase(); 42 | expect(API_BASE).toBe('https://example.com'); 43 | }); 44 | 45 | it('strips trailing slashes from VITE_API_URI', async () => { 46 | process.env.VITE_API_URI = 'https://example.com////'; 47 | const { API_BASE } = await loadApiBase(); 48 | expect(API_BASE).toBe('https://example.com'); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/proxy/processors/push-action/pullRemote.ts: -------------------------------------------------------------------------------- 1 | import { Action, Step } from '../../actions'; 2 | import fs from 'fs'; 3 | import git from 'isomorphic-git'; 4 | import gitHttpClient from 'isomorphic-git/http/node'; 5 | 6 | const dir = './.remote'; 7 | 8 | const exec = async (req: any, action: Action): Promise => { 9 | const step = new Step('pullRemote'); 10 | 11 | try { 12 | action.proxyGitPath = `${dir}/${action.id}`; 13 | 14 | if (!fs.existsSync(dir)) { 15 | fs.mkdirSync(dir); 16 | } 17 | 18 | if (!fs.existsSync(action.proxyGitPath)) { 19 | step.log(`Creating folder ${action.proxyGitPath}`); 20 | fs.mkdirSync(action.proxyGitPath, 0o755); 21 | } 22 | 23 | const cmd = `git clone ${action.url}`; 24 | step.log(`Executing ${cmd}`); 25 | 26 | const authHeader = req.headers?.authorization; 27 | const [username, password] = Buffer.from(authHeader.split(' ')[1], 'base64') 28 | .toString() 29 | .split(':'); 30 | 31 | // Note: setting singleBranch to true will cause issues when pushing to 32 | // a non-default branch as commits from those branches won't be fetched 33 | await git.clone({ 34 | fs, 35 | http: gitHttpClient, 36 | url: action.url, 37 | dir: `${action.proxyGitPath}/${action.repoName}`, 38 | onAuth: () => ({ username, password }), 39 | depth: 1, 40 | }); 41 | 42 | step.log(`Completed ${cmd}`); 43 | step.setContent(`Completed ${cmd}`); 44 | } catch (e: any) { 45 | step.setError(e.toString('utf-8')); 46 | throw e; 47 | } finally { 48 | action.addStep(step); 49 | } 50 | return action; 51 | }; 52 | 53 | exec.displayName = 'pullRemote.exec'; 54 | 55 | export { exec }; 56 | -------------------------------------------------------------------------------- /src/ui/assets/jss/material-dashboard-react/components/customInputStyle.js: -------------------------------------------------------------------------------- 1 | import { 2 | primaryColor, 3 | dangerColor, 4 | successColor, 5 | grayColor, 6 | defaultFont, 7 | } from '../../material-dashboard-react.js'; 8 | 9 | const customInputStyle = { 10 | disabled: { 11 | '&:before': { 12 | backgroundColor: 'transparent !important', 13 | }, 14 | }, 15 | underline: { 16 | '&:hover:not($disabled):before,&:before': { 17 | borderColor: grayColor[4] + ' !important', 18 | borderWidth: '1px !important', 19 | }, 20 | '&:after': { 21 | borderColor: primaryColor[0], 22 | }, 23 | }, 24 | underlineError: { 25 | '&:after': { 26 | borderColor: dangerColor[0], 27 | }, 28 | }, 29 | underlineSuccess: { 30 | '&:after': { 31 | borderColor: successColor[0], 32 | }, 33 | }, 34 | labelRoot: { 35 | ...defaultFont, 36 | color: grayColor[3] + ' !important', 37 | fontWeight: '400', 38 | fontSize: '14px', 39 | lineHeight: '1.42857', 40 | letterSpacing: 'unset', 41 | }, 42 | labelRootError: { 43 | color: dangerColor[0], 44 | }, 45 | labelRootSuccess: { 46 | color: successColor[0], 47 | }, 48 | feedback: { 49 | position: 'absolute', 50 | top: '18px', 51 | right: '0', 52 | zIndex: '2', 53 | display: 'block', 54 | width: '24px', 55 | height: '24px', 56 | textAlign: 'center', 57 | pointerEvents: 'none', 58 | }, 59 | marginTop: { 60 | marginTop: '16px', 61 | }, 62 | formControl: { 63 | paddingBottom: '10px', 64 | margin: '27px 0 0 0', 65 | position: 'relative', 66 | verticalAlign: 'unset', 67 | }, 68 | }; 69 | 70 | export default customInputStyle; 71 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | GitProxy supports responsible disclosure of security vulnerabilities and adheres to the [FINOS Security Vulnerabilities Policy](https://community.finos.org/docs/governance/Software-Projects/cve-responsible-disclosure). If you find something you believe to be a security issue in GitProxy, we encourage and appreciate your report. Please report the issue privately to the project maintainers using one of the following methods: 4 | 5 | ## Reporting a Vulnerability 6 | 7 | - **GitHub Security Reports:** In order for the vulnerability reports to reach maintainers as soon as possible, the preferred way is to use the ["Report a vulnerability"](https://github.com/finos/git-proxy/security/advisories) button under the "Security" tab of the associated GitHub project. This creates a private communication channel between the reporter and the maintainers. 8 | - **Email:** If you are unable to or have strong reasons not to use the GitHub Security vulnerability reporting feature, please email the maintainers and cc: [security@finos.org](mailto:security@finos.org) with a description of the vulnerability. 9 | 10 | ## Vulnerability Process 11 | 12 | 1. **Report the vulnerability privately** using one of the methods above. Do not create a public GitHub Issue or make any public reference to the vulnerability. 13 | 2. The project team will acknowledge receipt of your report and triage the issue. If a vulnerability is confirmed, the team will work with you to investigate and resolve it. 14 | 3. Once a fix is available, a release will be made and the vulnerability will be publicly disclosed in accordance with the [FINOS policy](https://community.finos.org/docs/governance/Software-Projects/cve-responsible-disclosure). 15 | -------------------------------------------------------------------------------- /experimental/license-inventory/src/services/data/license.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const chooseALicense = z.object({ 4 | permissions: z 5 | .object({ 6 | commercialUse: z.boolean().optional(), 7 | modifications: z.boolean().optional(), 8 | distribution: z.boolean().optional(), 9 | privateUse: z.boolean().optional(), 10 | patentUse: z.boolean().optional(), 11 | }) 12 | .optional(), 13 | conditions: z 14 | .object({ 15 | includeCopyright: z.boolean().optional(), 16 | includeCopyrightSource: z.boolean().optional(), 17 | documentChanges: z.boolean().optional(), 18 | discloseSource: z.boolean().optional(), 19 | networkUseDisclose: z.boolean().optional(), 20 | sameLicense: z.boolean().optional(), 21 | sameLicenseFile: z.boolean().optional(), 22 | sameLicenseLibrary: z.boolean().optional(), 23 | }) 24 | .optional(), 25 | limitations: z 26 | .object({ 27 | trademarkUse: z.boolean().optional(), 28 | liability: z.boolean().optional(), 29 | patentUse: z.boolean().optional(), 30 | warranty: z.boolean().optional(), 31 | }) 32 | .optional(), 33 | }); 34 | export type ChooseALicense = z.infer; 35 | 36 | export const license = z.object({ 37 | id: z.string().uuid(), 38 | name: z.string(), 39 | spdxID: z.string().optional(), 40 | chooseALicenseInfo: chooseALicense.optional(), 41 | }); 42 | export type License = z.infer; 43 | 44 | export const licenseNoID = license.omit({ id: true }); 45 | export type LicenseNoID = z.infer; 46 | 47 | export const licenseNoIDPartial = licenseNoID.partial(); 48 | export type LicenseNoIDPartial = z.infer; 49 | -------------------------------------------------------------------------------- /plugins/git-proxy-plugin-samples/example.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a sample plugin that logs a message when the pull action is called. It is written using 3 | * CommonJS modules to demonstrate the use of CommonJS in plugins. 4 | */ 5 | 6 | // Peer dependencies; its expected that these deps exist on Node module path if you've installed @finos/git-proxy 7 | const { PushActionPlugin } = require('@finos/git-proxy/src/plugin'); 8 | const { Step } = require('@finos/git-proxy/src/proxy/actions'); 9 | 'use strict'; 10 | 11 | /** 12 | * 13 | * @param {object} req Express Request object 14 | * @param {Action} action GitProxy Action 15 | * @return {Promise} Promise that resolves to an Action 16 | */ 17 | async function logMessage(req, action) { 18 | const step = new Step('LogRequestPlugin'); 19 | action.addStep(step); 20 | console.log(`LogRequestPlugin: req url ${req.url}`); 21 | console.log(`LogRequestPlugin: req user-agent ${req.header('User-Agent')}`); 22 | console.log('LogRequestPlugin: action', JSON.stringify(action)); 23 | return action; 24 | } 25 | 26 | class LogRequestPlugin extends PushActionPlugin { 27 | constructor() { 28 | super(logMessage) 29 | } 30 | } 31 | 32 | 33 | module.exports = { 34 | // Plugins can be written inline as new instances of Push/PullActionPlugin 35 | // A custom class is not required 36 | hello: new PushActionPlugin(async (req, action) => { 37 | const step = new Step('HelloPlugin'); 38 | action.addStep(step); 39 | console.log('Hello world from the hello plugin!'); 40 | return action; 41 | }), 42 | // Sub-classing is fine too if you require more control over the plugin 43 | logRequest: new LogRequestPlugin(), 44 | someOtherValue: 'foo', // This key will be ignored by the plugin loader 45 | }; 46 | -------------------------------------------------------------------------------- /src/ui/components/CustomButtons/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import Button, { ButtonProps } from '@material-ui/core/Button'; 5 | import styles from '../../assets/jss/material-dashboard-react/components/buttonStyle'; 6 | 7 | const useStyles = makeStyles(styles); 8 | 9 | type Color = 10 | | 'primary' 11 | | 'info' 12 | | 'success' 13 | | 'warning' 14 | | 'danger' 15 | | 'rose' 16 | | 'white' 17 | | 'transparent'; 18 | type Size = 'sm' | 'lg'; 19 | 20 | interface RegularButtonProps extends Omit { 21 | color?: Color; 22 | round?: boolean; 23 | disabled?: boolean; 24 | simple?: boolean; 25 | size?: Size; 26 | block?: boolean; 27 | link?: boolean; 28 | justIcon?: boolean; 29 | className?: string; 30 | muiClasses?: Record; 31 | children?: React.ReactNode; 32 | } 33 | 34 | export default function RegularButton(props: RegularButtonProps) { 35 | const classes = useStyles(); 36 | const { 37 | color, 38 | round, 39 | children, 40 | disabled, 41 | simple, 42 | size, 43 | block, 44 | link, 45 | justIcon, 46 | className, 47 | muiClasses, 48 | ...rest 49 | } = props; 50 | 51 | const btnClasses = clsx({ 52 | [classes.button]: true, 53 | [classes[size as Size]]: size, 54 | [classes[color as Color]]: color, 55 | [classes.round]: round, 56 | [classes.disabled]: disabled, 57 | [classes.simple]: simple, 58 | [classes.block]: block, 59 | [classes.link]: link, 60 | [classes.justIcon]: justIcon, 61 | [className || '']: className, 62 | }); 63 | 64 | return ( 65 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tsx 2 | 3 | import path from 'path'; 4 | import yargs from 'yargs'; 5 | import { hideBin } from 'yargs/helpers'; 6 | import * as fs from 'fs'; 7 | import { getConfigFile, setConfigFile, validate } from './src/config/file'; 8 | import { initUserConfig } from './src/config'; 9 | import { Proxy } from './src/proxy'; 10 | import { Service } from './src/service'; 11 | 12 | const argv = yargs(hideBin(process.argv)) 13 | .usage('Usage: $0 [options]') 14 | .options({ 15 | validate: { 16 | description: 17 | 'Check the proxy.config.json file in the current working directory for validation errors.', 18 | required: false, 19 | alias: 'v', 20 | type: 'boolean', 21 | }, 22 | config: { 23 | description: 'Path to custom git-proxy configuration file.', 24 | default: path.join(__dirname, 'proxy.config.json'), 25 | required: false, 26 | alias: 'c', 27 | type: 'string', 28 | }, 29 | }) 30 | .strict() 31 | .parseSync(); 32 | 33 | console.log('Setting config file to: ' + (argv.c as string) || ''); 34 | setConfigFile((argv.c as string) || ''); 35 | initUserConfig(); 36 | 37 | const configFile = getConfigFile(); 38 | if (argv.v) { 39 | if (!fs.existsSync(configFile)) { 40 | console.error( 41 | `Config file ${configFile} doesn't exist, nothing to validate! Did you forget -c/--config?`, 42 | ); 43 | process.exit(1); 44 | } 45 | 46 | validate(); 47 | console.log(`${configFile} is valid`); 48 | process.exit(0); 49 | } 50 | 51 | console.log('validating config'); 52 | validate(); 53 | 54 | console.log('Setting up the proxy and Service'); 55 | 56 | // The deferred imports should cause these to be loaded on first access 57 | const proxy = new Proxy(); 58 | proxy.start(); 59 | Service.start(proxy); 60 | 61 | export { proxy, Service }; 62 | -------------------------------------------------------------------------------- /src/ui/assets/jss/material-dashboard-react/components/tableStyle.js: -------------------------------------------------------------------------------- 1 | import { 2 | warningColor, 3 | primaryColor, 4 | dangerColor, 5 | successColor, 6 | infoColor, 7 | roseColor, 8 | grayColor, 9 | defaultFont, 10 | } from '../../material-dashboard-react.js'; 11 | 12 | const tableStyle = (theme) => ({ 13 | warningTableHeader: { 14 | color: warningColor[0], 15 | }, 16 | primaryTableHeader: { 17 | color: primaryColor[0], 18 | }, 19 | dangerTableHeader: { 20 | color: dangerColor[0], 21 | }, 22 | successTableHeader: { 23 | color: successColor[0], 24 | }, 25 | infoTableHeader: { 26 | color: infoColor[0], 27 | }, 28 | roseTableHeader: { 29 | color: roseColor[0], 30 | }, 31 | grayTableHeader: { 32 | color: grayColor[0], 33 | }, 34 | table: { 35 | marginBottom: '0', 36 | width: '100%', 37 | maxWidth: '100%', 38 | backgroundColor: 'transparent', 39 | borderSpacing: '0', 40 | borderCollapse: 'collapse', 41 | }, 42 | tableHeadCell: { 43 | color: 'inherit', 44 | ...defaultFont, 45 | '&, &$tableCell': { 46 | fontSize: '1em', 47 | }, 48 | }, 49 | tableCell: { 50 | ...defaultFont, 51 | lineHeight: '1.42857143', 52 | padding: '12px 8px', 53 | verticalAlign: 'middle', 54 | fontSize: '0.8125rem', 55 | }, 56 | tableResponsive: { 57 | width: '100%', 58 | marginTop: theme.spacing(3), 59 | overflowX: 'auto', 60 | }, 61 | tableHeadRow: { 62 | height: '56px', 63 | color: 'inherit', 64 | display: 'table-row', 65 | outline: 'none', 66 | verticalAlign: 'middle', 67 | }, 68 | tableBodyRow: { 69 | height: '48px', 70 | color: 'inherit', 71 | display: 'table-row', 72 | outline: 'none', 73 | verticalAlign: 'middle', 74 | }, 75 | }); 76 | 77 | export default tableStyle; 78 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | This website was created with [Docusaurus v2](https://v2.docusaurus.io/). 2 | 3 | In order to start working with Docusaurus, please read the [Getting Started guide](https://docusaurus.io/docs/configuration) and browse through the following folders and files: 4 | 5 | - `website` - contains the Node/React code to build the website 6 | - `website/docusaurus.config.js` - contains the Docusaurus configuration; you'll need to edit this file. 7 | - `website/static` - contains images, PDF and other static assets used in the website; if you add a `file.pdf` in this folder, it will be served as `https:///file.pdf`. 8 | - `docs` - contains the `.md` and `.mdx` files that are served as `https:///` ; the `file_id` is defined at the top of the file. 9 | 10 | ## Local run 11 | 12 | Running Docusaurus locally is very simple, just follow these steps: 13 | 14 | - Make sure `node` version is 14 or higher, using `node -v` ; you can use [nvm](https://github.com/nvm-sh/nvm) to install different node versions in your system. 15 | - `cd website ; npm install ; npm run start` 16 | 17 | The command should open your browser and point to `http://localhost:8080`. 18 | 19 | ## Deployment 20 | 21 | [Netlify] (https://www.netlify.com/) is the default way to serve FINOS websites publicly. Find docs [here] (https://docs.netlify.com/configure-builds/get-started/). 22 | 23 | You can configure Netlify using your own GitHub account, pointing to a personal repository (or fork); when adding a new site, please use the following configuration: 24 | 25 | - Woeking directory: `website` 26 | - Build command: `yarn build` 27 | - Build directory: `website/build` 28 | 29 | If you want to serve your website through `https://.finos.org`, please email [help@finos.org](mailto:help@finos.org). To check a preview, visit https://project-blueprint.finos.org . 30 | -------------------------------------------------------------------------------- /src/ui/components/RouteGuard/RouteGuard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Navigate } from 'react-router-dom'; 3 | import { useAuth } from '../../auth/AuthProvider'; 4 | import { setUIRouteAuthData } from '../../services/config'; 5 | import { UIRouteAuth } from '../../../config/generated/config'; 6 | import CircularProgress from '@material-ui/core/CircularProgress'; 7 | 8 | interface RouteGuardProps { 9 | component: React.ComponentType; 10 | fullRoutePath: string; 11 | } 12 | 13 | const RouteGuard = ({ component: Component, fullRoutePath }: RouteGuardProps) => { 14 | const { user, isLoading } = useAuth(); 15 | 16 | const [loginRequired, setLoginRequired] = useState(false); 17 | const [adminOnly, setAdminOnly] = useState(false); 18 | const [authChecked, setAuthChecked] = useState(false); 19 | 20 | useEffect(() => { 21 | setUIRouteAuthData((uiRouteAuth: UIRouteAuth) => { 22 | if (uiRouteAuth?.enabled) { 23 | for (const rule of uiRouteAuth.rules ?? []) { 24 | if (new RegExp(rule.pattern ?? '').test(fullRoutePath)) { 25 | // Allow multiple rules to be applied according to route precedence 26 | // Ex: /dashboard/admin/* will override /dashboard/* 27 | setLoginRequired(loginRequired || rule.loginRequired || false); 28 | setAdminOnly(adminOnly || rule.adminOnly || false); 29 | } 30 | } 31 | } 32 | setAuthChecked(true); 33 | }); 34 | }, [fullRoutePath]); 35 | 36 | if (!authChecked || isLoading) { 37 | return ; 38 | } 39 | 40 | if (loginRequired && !user) { 41 | return ; 42 | } 43 | 44 | if (adminOnly && !user?.admin) { 45 | return ; 46 | } 47 | 48 | return ; 49 | }; 50 | 51 | export default RouteGuard; 52 | -------------------------------------------------------------------------------- /src/ui/components/Snackbar/SnackbarContent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import MuiSnackbarContent from '@material-ui/core/SnackbarContent'; 5 | import IconButton from '@material-ui/core/IconButton'; 6 | import Close from '@material-ui/icons/Close'; 7 | import styles from '../../assets/jss/material-dashboard-react/components/snackbarContentStyle'; 8 | 9 | const useStyles = makeStyles(styles); 10 | 11 | type Color = 'info' | 'success' | 'warning' | 'danger' | 'primary'; 12 | 13 | interface SnackbarContentProps { 14 | message: React.ReactNode; 15 | color?: Color; 16 | close?: boolean; 17 | icon?: React.ComponentType<{ className: string }>; 18 | rtlActive?: boolean; 19 | } 20 | 21 | const SnackbarContent: React.FC = (props) => { 22 | const classes = useStyles(); 23 | const { message, color = 'info', close, icon: Icon, rtlActive } = props; 24 | 25 | let action: React.ReactNode[] = []; 26 | const messageClasses = clsx({ 27 | [classes.iconMessage]: Icon !== undefined, 28 | }); 29 | 30 | if (close) { 31 | action = [ 32 | 33 | 34 | , 35 | ]; 36 | } 37 | 38 | return ( 39 | 42 | {Icon && } 43 | {message} 44 | 45 | } 46 | classes={{ 47 | root: clsx(classes.root, classes[color]), 48 | message: classes.message, 49 | action: clsx({ [classes.actionRTL]: rtlActive }), 50 | }} 51 | action={action} 52 | /> 53 | ); 54 | }; 55 | 56 | export default SnackbarContent; 57 | -------------------------------------------------------------------------------- /experimental/license-inventory/README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | Logo 5 | 6 | 7 |
8 | git-proxy: license-inventory 9 |
10 | 11 |

12 | experimental project to provide license data and attach said license data to projects for fueling policy evaluation. 13 |
14 |

15 | 16 |
17 | 18 | [![FINOS - Incubating](https://cdn.jsdelivr.net/gh/finos/contrib-toolbox@master/images/badge-incubating.svg)](https://community.finos.org/docs/governance/Software-Projects/stages/incubating) 19 | [![NPM](https://img.shields.io/npm/v/@finos/git-proxy-license-inventory?colorA=00C586&colorB=000000)](https://www.npmjs.com/package/@finos/git-proxy-license-inventory) 20 | [![Build](https://img.shields.io/github/actions/workflow/status/finos/git-proxy/experimental-inventory-ci.yml?branch=main&label=CI&logo=github&colorA=00C586&colorB=000000)](https://github.com/finos/git-proxy/actions/workflows/experimental-inventory-ci.yml) 21 | [![git-proxy](https://api.securityscorecards.dev/projects/github.com/finos/git-proxy/badge)](https://api.securityscorecards.dev/projects/github.com/finos/git-proxy) 22 |
23 | [![License](https://img.shields.io/github/license/finos/git-proxy?colorA=00C586&colorB=000000)](https://github.com/finos/git-proxy/blob/main/LICENSE) 24 | [![Slack](https://img.shields.io/badge/_-Chat_on_Slack-000000.svg?logo=slack&colorA=00C586)](https://app.slack.com/client/T01E7QRQH97/C06LXNW0W76) 25 | 26 |
27 |
28 | 29 | This is an **experimental** project to provide license data and attach said license data to projects for fueling policy evaluation. 30 | 31 | Please consider all REST API paths, inputs, and outputs in flux during `v0`. Additionally do not rely import paths to be stable. 32 | -------------------------------------------------------------------------------- /src/ui/services/auth.ts: -------------------------------------------------------------------------------- 1 | import { getCookie } from '../utils'; 2 | import { PublicUser } from '../../db/types'; 3 | import { API_BASE } from '../apiBase'; 4 | import { AxiosError } from 'axios'; 5 | 6 | interface AxiosConfig { 7 | withCredentials: boolean; 8 | headers: { 9 | 'X-CSRF-TOKEN': string; 10 | Authorization?: string; 11 | }; 12 | } 13 | 14 | /** 15 | * Gets the current user's information 16 | */ 17 | export const getUserInfo = async (): Promise => { 18 | try { 19 | const response = await fetch(`${API_BASE}/api/auth/profile`, { 20 | credentials: 'include', // Sends cookies 21 | }); 22 | if (!response.ok) throw new Error(`Failed to fetch user info: ${response.statusText}`); 23 | return await response.json(); 24 | } catch (error) { 25 | console.error('Error fetching user info:', error); 26 | return null; 27 | } 28 | }; 29 | 30 | /** 31 | * Gets the Axios config for the UI 32 | */ 33 | export const getAxiosConfig = (): AxiosConfig => { 34 | const jwtToken = localStorage.getItem('ui_jwt_token'); 35 | return { 36 | withCredentials: true, 37 | headers: { 38 | 'X-CSRF-TOKEN': getCookie('csrf') || '', 39 | Authorization: jwtToken ? `Bearer ${jwtToken}` : undefined, 40 | }, 41 | }; 42 | }; 43 | 44 | /** 45 | * Processes authentication errors and returns a user-friendly error message 46 | */ 47 | export const processAuthError = (error: AxiosError, jwtAuthEnabled = false): string => { 48 | const errorMessage = (error.response?.data as any)?.message ?? 'Unknown error'; 49 | let msg = `Failed to authorize user: ${errorMessage.trim()}. `; 50 | if (jwtAuthEnabled && !localStorage.getItem('ui_jwt_token')) { 51 | msg += 'Set your JWT token in the settings page or disable JWT auth in your app configuration.'; 52 | } else { 53 | msg += 'Check your JWT token or disable JWT auth in your app configuration.'; 54 | } 55 | return msg; 56 | }; 57 | -------------------------------------------------------------------------------- /website/src/pages/testimonials.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 3 | import { LinkedInEmbed } from 'react-social-media-embed'; 4 | import Slider from 'react-slick'; 5 | import 'slick-carousel/slick/slick.css'; 6 | import 'slick-carousel/slick/slick-theme.css'; 7 | 8 | /** 9 | * Home page component 10 | * @return {JSX.Element} 11 | */ 12 | function Testimonials() { 13 | const context = useDocusaurusContext(); 14 | const { siteConfig = {} } = context; 15 | const { posts } = siteConfig.customFields; 16 | const settings = { 17 | infinite: true, 18 | speed: 500, 19 | slidesToShow: 3, 20 | slidesToScroll: 3, 21 | swipeToSlide: true, 22 | swipe: true, 23 | responsive: [ 24 | { 25 | breakpoint: 1024, 26 | settings: { 27 | slidesToShow: 2, 28 | slidesToScroll: 2, 29 | infinite: true, 30 | dots: true, 31 | }, 32 | }, 33 | { 34 | breakpoint: 800, 35 | settings: { 36 | slidesToShow: 1, 37 | slidesToScroll: 1, 38 | initialSlide: 1, 39 | }, 40 | }, 41 | ], 42 | dots: true, 43 | arrows: true, 44 | }; 45 | 46 | return ( 47 |
48 |
49 |

Testimonials 📣

50 |
51 | 52 |
53 | 54 | {posts 55 | .filter((post) => post.platform === 'linkedin') 56 | .map((post) => { 57 | return ( 58 |
59 | 60 |
61 | ); 62 | })} 63 |
64 |
65 |
66 | ); 67 | } 68 | 69 | export default Testimonials; 70 | -------------------------------------------------------------------------------- /test/services/routes/users.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; 2 | import express, { Express } from 'express'; 3 | import request from 'supertest'; 4 | import usersRouter from '../../../src/service/routes/users'; 5 | import * as db from '../../../src/db'; 6 | 7 | describe('Users API', () => { 8 | let app: Express; 9 | 10 | beforeEach(() => { 11 | app = express(); 12 | app.use(express.json()); 13 | app.use('/users', usersRouter); 14 | 15 | vi.spyOn(db, 'getUsers').mockResolvedValue([ 16 | { 17 | username: 'alice', 18 | password: 'secret-hashed-password', 19 | email: 'alice@example.com', 20 | displayName: 'Alice Walker', 21 | }, 22 | ] as any); 23 | 24 | vi.spyOn(db, 'findUser').mockResolvedValue({ 25 | username: 'bob', 26 | password: 'hidden', 27 | email: 'bob@example.com', 28 | } as any); 29 | }); 30 | 31 | afterEach(() => { 32 | vi.restoreAllMocks(); 33 | }); 34 | 35 | it('GET /users only serializes public data needed for ui, not user secrets like password', async () => { 36 | const res = await request(app).get('/users'); 37 | 38 | expect(res.status).toBe(200); 39 | expect(res.body).toEqual([ 40 | { 41 | username: 'alice', 42 | displayName: 'Alice Walker', 43 | email: 'alice@example.com', 44 | title: '', 45 | gitAccount: '', 46 | admin: false, 47 | }, 48 | ]); 49 | }); 50 | 51 | it('GET /users/:id does not serialize password', async () => { 52 | const res = await request(app).get('/users/bob'); 53 | 54 | expect(res.status).toBe(200); 55 | console.log(`Response body: ${JSON.stringify(res.body)}`); 56 | expect(res.body).toEqual({ 57 | username: 'bob', 58 | displayName: '', 59 | email: 'bob@example.com', 60 | title: '', 61 | gitAccount: '', 62 | admin: false, 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/db/mongo/repo.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest'; 2 | import { Repo } from '../../../src/db/types'; 3 | 4 | const mockFindOne = vi.fn(); 5 | const mockConnect = vi.fn(() => ({ 6 | findOne: mockFindOne, 7 | })); 8 | 9 | vi.mock('../../../src/db/mongo/helper', () => ({ 10 | connect: mockConnect, 11 | })); 12 | 13 | describe('MongoDB', async () => { 14 | const { getRepo, getRepoByUrl } = await import('../../../src/db/mongo/repo'); 15 | 16 | beforeEach(() => { 17 | vi.clearAllMocks(); 18 | }); 19 | 20 | afterEach(() => { 21 | vi.restoreAllMocks(); 22 | }); 23 | 24 | describe('getRepo', () => { 25 | it('should get the repo using the name', async () => { 26 | const repoData: Partial = { 27 | name: 'sample', 28 | users: { canPush: [], canAuthorise: [] }, 29 | url: 'http://example.com/sample-repo.git', 30 | }; 31 | 32 | mockFindOne.mockResolvedValue(repoData); 33 | 34 | const result = await getRepo('Sample'); 35 | 36 | expect(result).toEqual(repoData); 37 | expect(mockConnect).toHaveBeenCalledWith('repos'); 38 | expect(mockFindOne).toHaveBeenCalledWith({ name: { $eq: 'sample' } }); 39 | }); 40 | }); 41 | 42 | describe('getRepoByUrl', () => { 43 | it('should get the repo using the url', async () => { 44 | const repoData: Partial = { 45 | name: 'sample', 46 | users: { canPush: [], canAuthorise: [] }, 47 | url: 'https://github.com/finos/git-proxy.git', 48 | }; 49 | 50 | mockFindOne.mockResolvedValue(repoData); 51 | 52 | const result = await getRepoByUrl('https://github.com/finos/git-proxy.git'); 53 | 54 | expect(result).toEqual(repoData); 55 | expect(mockConnect).toHaveBeenCalledWith('repos'); 56 | expect(mockFindOne).toHaveBeenCalledWith({ 57 | url: { $eq: 'https://github.com/finos/git-proxy.git' }, 58 | }); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/proxy/processors/push-action/checkAuthorEmails.ts: -------------------------------------------------------------------------------- 1 | import { Action, Step } from '../../actions'; 2 | import { getCommitConfig } from '../../../config'; 3 | import { CommitData } from '../types'; 4 | import { isEmail } from 'validator'; 5 | 6 | const isEmailAllowed = (email: string): boolean => { 7 | const commitConfig = getCommitConfig(); 8 | 9 | if (!email || !isEmail(email)) { 10 | return false; 11 | } 12 | 13 | const [emailLocal, emailDomain] = email.split('@'); 14 | 15 | if ( 16 | commitConfig?.author?.email?.domain?.allow && 17 | !new RegExp(commitConfig.author.email.domain.allow, 'gi').test(emailDomain) 18 | ) { 19 | return false; 20 | } 21 | 22 | if ( 23 | commitConfig?.author?.email?.local?.block && 24 | new RegExp(commitConfig.author.email.local.block, 'gi').test(emailLocal) 25 | ) { 26 | return false; 27 | } 28 | 29 | return true; 30 | }; 31 | 32 | const exec = async (req: any, action: Action): Promise => { 33 | const step = new Step('checkAuthorEmails'); 34 | 35 | const uniqueAuthorEmails = [ 36 | ...new Set(action.commitData?.map((commitData: CommitData) => commitData.authorEmail)), 37 | ]; 38 | 39 | const illegalEmails = uniqueAuthorEmails.filter((email) => !isEmailAllowed(email)); 40 | 41 | if (illegalEmails.length > 0) { 42 | console.log(`The following commit author e-mails are illegal: ${illegalEmails}`); 43 | 44 | step.error = true; 45 | step.log(`The following commit author e-mails are illegal: ${illegalEmails}`); 46 | step.setError( 47 | 'Your push has been blocked. Please verify your Git configured e-mail address is valid (e.g. john.smith@example.com)', 48 | ); 49 | 50 | action.addStep(step); 51 | return action; 52 | } 53 | 54 | console.log(`The following commit author e-mails are legal: ${uniqueAuthorEmails}`); 55 | action.addStep(step); 56 | return action; 57 | }; 58 | 59 | exec.displayName = 'checkAuthorEmails.exec'; 60 | 61 | export { exec }; 62 | -------------------------------------------------------------------------------- /test/testAuthMethods.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach } from 'vitest'; 2 | 3 | describe('auth methods', () => { 4 | beforeEach(() => { 5 | vi.resetModules(); 6 | }); 7 | 8 | it('should return a local auth method by default', async () => { 9 | const config = await import('../src/config'); 10 | const authMethods = config.getAuthMethods(); 11 | expect(authMethods).toHaveLength(1); 12 | expect(authMethods[0].type).toBe('local'); 13 | }); 14 | 15 | it('should return an error if no auth methods are enabled', async () => { 16 | const newConfig = JSON.stringify({ 17 | authentication: [ 18 | { type: 'local', enabled: false }, 19 | { type: 'ActiveDirectory', enabled: false }, 20 | { type: 'openidconnect', enabled: false }, 21 | ], 22 | }); 23 | 24 | vi.doMock('fs', () => ({ 25 | existsSync: () => true, 26 | readFileSync: () => newConfig, 27 | })); 28 | 29 | const config = await import('../src/config'); 30 | config.initUserConfig(); 31 | 32 | expect(() => config.getAuthMethods()).toThrowError(/No authentication method enabled/); 33 | }); 34 | 35 | it('should return an array of enabled auth methods when overridden', async () => { 36 | const newConfig = JSON.stringify({ 37 | authentication: [ 38 | { type: 'local', enabled: true }, 39 | { type: 'ActiveDirectory', enabled: true }, 40 | { type: 'openidconnect', enabled: true }, 41 | ], 42 | }); 43 | 44 | vi.doMock('fs', () => ({ 45 | existsSync: () => true, 46 | readFileSync: () => newConfig, 47 | })); 48 | 49 | const config = await import('../src/config'); 50 | config.initUserConfig(); 51 | 52 | const authMethods = config.getAuthMethods(); 53 | expect(authMethods).toHaveLength(3); 54 | expect(authMethods[0].type).toBe('local'); 55 | expect(authMethods[1].type).toBe('ActiveDirectory'); 56 | expect(authMethods[2].type).toBe('openidconnect'); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/proxy/processors/pre-processor/parseAction.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '../../actions'; 2 | import { processUrlPath } from '../../routes/helper'; 3 | import * as db from '../../../db'; 4 | 5 | const exec = async (req: { 6 | originalUrl: string; 7 | method: string; 8 | headers: Record; 9 | }) => { 10 | const id = Date.now(); 11 | const timestamp = id; 12 | let type = 'default'; 13 | 14 | //inspect content-type headers to classify requests as push or pull operations 15 | // see git http protocol docs for more details: https://github.com/git/git/blob/master/Documentation/gitprotocol-http.adoc 16 | if (req.headers['content-type'] === 'application/x-git-upload-pack-request') { 17 | type = 'pull'; 18 | } else if (req.headers['content-type'] === 'application/x-git-receive-pack-request') { 19 | type = 'push'; 20 | } 21 | 22 | // Proxy URLs take the form https://:// 23 | // e.g. https://git-proxy-instance.com:8443/github.com/finos/git-proxy.git 24 | // We'll receive /github.com/finos/git-proxy.git as the req.url / req.originalUrl 25 | // Add protocol (assume SSL) to reconstruct full URL - noting path will start with a / 26 | const pathBreakdown = processUrlPath(req.originalUrl); 27 | let url = 'https:/' + (pathBreakdown?.repoPath ?? 'NOT-FOUND'); 28 | 29 | console.log(`Parse action calculated repo URL: ${url} for inbound URL path: ${req.originalUrl}`); 30 | 31 | if (!(await db.getRepoByUrl(url))) { 32 | // fallback for legacy proxy URLs 33 | // legacy git proxy paths took the form: https://:/ 34 | // by assuming the host was github.com 35 | url = 'https://github.com' + (pathBreakdown?.repoPath ?? 'NOT-FOUND'); 36 | console.log( 37 | `Parse action fallback calculated repo URL: ${url} for inbound URL path: ${req.originalUrl}`, 38 | ); 39 | } 40 | 41 | return new Action(id.toString(), type, req.method, timestamp, url); 42 | }; 43 | 44 | exec.displayName = 'parseAction.exec'; 45 | 46 | export { exec }; 47 | -------------------------------------------------------------------------------- /src/service/passport/ldaphelper.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import type { Request } from 'express'; 3 | import ActiveDirectory from 'activedirectory2'; 4 | import { getAPIs } from '../../config'; 5 | import { ADProfile } from './types'; 6 | 7 | const thirdpartyApiConfig = getAPIs(); 8 | 9 | export const isUserInAdGroup = ( 10 | req: Request & { user?: ADProfile }, 11 | profile: ADProfile, 12 | ad: ActiveDirectory, 13 | domain: string, 14 | name: string, 15 | ): Promise => { 16 | // determine, via config, if we're using HTTP or AD directly 17 | if (thirdpartyApiConfig.ls?.userInADGroup) { 18 | return isUserInAdGroupViaHttp(profile.username || '', domain, name); 19 | } else { 20 | return isUserInAdGroupViaAD(req, profile, ad, domain, name); 21 | } 22 | }; 23 | 24 | const isUserInAdGroupViaAD = ( 25 | req: Request & { user?: ADProfile }, 26 | profile: ADProfile, 27 | ad: ActiveDirectory, 28 | domain: string, 29 | name: string, 30 | ): Promise => { 31 | return new Promise((resolve, reject) => { 32 | ad.isUserMemberOf(profile.username || '', name, function (err, isMember) { 33 | if (err) { 34 | const msg = 'ERROR isUserMemberOf: ' + JSON.stringify(err); 35 | reject(msg); 36 | } else { 37 | console.log(profile.username + ' isMemberOf ' + name + ': ' + isMember); 38 | resolve(isMember); 39 | } 40 | }); 41 | }); 42 | }; 43 | 44 | const isUserInAdGroupViaHttp = (id: string, domain: string, name: string): Promise => { 45 | const url = String(thirdpartyApiConfig.ls?.userInADGroup) 46 | .replace('', domain) 47 | .replace('', name) 48 | .replace('', id); 49 | 50 | const client = axios.create({ 51 | responseType: 'json', 52 | headers: { 53 | 'content-type': 'application/json', 54 | }, 55 | }); 56 | 57 | console.log(`checking if user is in group ${url}`); 58 | return client 59 | .get(url) 60 | .then((res) => Boolean(res.data)) 61 | .catch(() => { 62 | return false; 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /test/db/db.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest'; 2 | 3 | vi.mock('../../src/db/mongo', () => ({ 4 | getRepoByUrl: vi.fn(), 5 | })); 6 | 7 | vi.mock('../../src/db/file', () => ({ 8 | getRepoByUrl: vi.fn(), 9 | })); 10 | 11 | vi.mock('../../src/config', () => ({ 12 | getDatabase: vi.fn(() => ({ type: 'mongo' })), 13 | })); 14 | 15 | import * as db from '../../src/db'; 16 | import * as mongo from '../../src/db/mongo'; 17 | 18 | describe('db', () => { 19 | beforeEach(() => { 20 | vi.clearAllMocks(); 21 | }); 22 | 23 | afterEach(() => { 24 | vi.restoreAllMocks(); 25 | }); 26 | 27 | describe('isUserPushAllowed', () => { 28 | it('returns true if user is in canPush', async () => { 29 | vi.mocked(mongo.getRepoByUrl).mockResolvedValue({ 30 | users: { 31 | canPush: ['alice'], 32 | canAuthorise: [], 33 | }, 34 | } as any); 35 | 36 | const result = await db.isUserPushAllowed('myrepo', 'alice'); 37 | expect(result).toBe(true); 38 | }); 39 | 40 | it('returns true if user is in canAuthorise', async () => { 41 | vi.mocked(mongo.getRepoByUrl).mockResolvedValue({ 42 | users: { 43 | canPush: [], 44 | canAuthorise: ['bob'], 45 | }, 46 | } as any); 47 | 48 | const result = await db.isUserPushAllowed('myrepo', 'bob'); 49 | expect(result).toBe(true); 50 | }); 51 | 52 | it('returns false if user is in neither', async () => { 53 | vi.mocked(mongo.getRepoByUrl).mockResolvedValue({ 54 | users: { 55 | canPush: [], 56 | canAuthorise: [], 57 | }, 58 | } as any); 59 | 60 | const result = await db.isUserPushAllowed('myrepo', 'charlie'); 61 | expect(result).toBe(false); 62 | }); 63 | 64 | it('returns false if repo is not registered', async () => { 65 | vi.mocked(mongo.getRepoByUrl).mockResolvedValue(null); 66 | 67 | const result = await db.isUserPushAllowed('myrepo', 'charlie'); 68 | expect(result).toBe(false); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /website/docs/configuration/pre-receive.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Pre-Receive Hook' 3 | --- 4 | 5 | ## Overview 6 | 7 | The `pre-receive` hook is a critical component of the GitProxy system. It is executed before changes are accepted into a repository. This hook allows for custom logic to validate or reject incoming changes based on specific criteria, ensuring that only valid and authorized changes are pushed to the repository. 8 | 9 | ## Functionality 10 | 11 | The `pre-receive` hook determines the outcome of a push based on the exit status of the hook script: 12 | 13 | - If the script exits with status `0`, the push is automatically approved. 14 | - If the script exits with status `1`, the push is automatically rejected. 15 | - If the script exits with status `2`, the push requires manual approval. 16 | - Any other exit status is treated as an error, and the push is rejected with an appropriate error message. 17 | 18 | ## Usage 19 | 20 | To use the `pre-receive` hook, follow these steps: 21 | 22 | ### Create a Hook Script 23 | 24 | Write a shell script or executable file that implements your custom validation logic. The script must accept input in the format: ` `. 25 | 26 | ### Place the Script 27 | 28 | Save the script in the appropriate directory, such as `hooks/pre-receive.sh`. 29 | 30 | ### Make the Script Executable 31 | 32 | Ensure the script has executable permissions. You can do this by running the following command: 33 | 34 | ```bash 35 | chmod +x hooks/pre-receive.sh 36 | ``` 37 | 38 | > **Note**: If the `pre-receive` script does not exist, the hook will not be executed, and the push will proceed without validation. 39 | 40 | ## Example Hook Script 41 | 42 | Below is an example of a simple `pre-receive` hook script: 43 | 44 | ```bash 45 | #!/bin/bash 46 | 47 | read old_commit new_commit branch_name 48 | 49 | # Example validation: Reject pushes to the main branch 50 | if [ "$branch_name" == "main" ]; then 51 | echo "Pushes to the main branch are not allowed." 52 | exit 1 53 | fi 54 | 55 | # Approve all other pushes 56 | exit 0 57 | ``` 58 | -------------------------------------------------------------------------------- /src/ui/assets/jss/material-dashboard-react/views/dashboardStyle.js: -------------------------------------------------------------------------------- 1 | import { successColor, whiteColor, grayColor, hexToRgb } from '../../material-dashboard-react.js'; 2 | 3 | const dashboardStyle = { 4 | successText: { 5 | color: successColor[0], 6 | }, 7 | upArrowCardCategory: { 8 | width: '16px', 9 | height: '16px', 10 | }, 11 | stats: { 12 | color: grayColor[0], 13 | display: 'inline-flex', 14 | fontSize: '12px', 15 | lineHeight: '22px', 16 | '& svg': { 17 | top: '4px', 18 | width: '16px', 19 | height: '16px', 20 | position: 'relative', 21 | marginRight: '3px', 22 | marginLeft: '3px', 23 | }, 24 | '& .fab,& .fas,& .far,& .fal,& .material-icons': { 25 | top: '4px', 26 | fontSize: '16px', 27 | position: 'relative', 28 | marginRight: '3px', 29 | marginLeft: '3px', 30 | }, 31 | }, 32 | cardCategory: { 33 | color: grayColor[0], 34 | margin: '0', 35 | fontSize: '14px', 36 | marginTop: '0', 37 | paddingTop: '10px', 38 | marginBottom: '0', 39 | }, 40 | cardCategoryWhite: { 41 | color: 'rgba(' + hexToRgb(whiteColor) + ',.62)', 42 | margin: '0', 43 | fontSize: '14px', 44 | marginTop: '0', 45 | marginBottom: '0', 46 | }, 47 | cardTitle: { 48 | color: grayColor[2], 49 | marginTop: '0px', 50 | minHeight: 'auto', 51 | fontWeight: '300', 52 | fontFamily: "'Roboto', 'Helvetica', 'Arial', sans-serif", 53 | marginBottom: '3px', 54 | textDecoration: 'none', 55 | '& small': { 56 | color: grayColor[1], 57 | fontWeight: '400', 58 | lineHeight: '1', 59 | }, 60 | }, 61 | cardTitleWhite: { 62 | color: whiteColor, 63 | marginTop: '0px', 64 | minHeight: 'auto', 65 | fontWeight: '300', 66 | fontFamily: "'Roboto', 'Helvetica', 'Arial', sans-serif", 67 | marginBottom: '3px', 68 | textDecoration: 'none', 69 | '& small': { 70 | color: grayColor[1], 71 | fontWeight: '400', 72 | lineHeight: '1', 73 | }, 74 | }, 75 | }; 76 | 77 | export default dashboardStyle; 78 | -------------------------------------------------------------------------------- /certs/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFnzCCA4egAwIBAgIUP3XfqkVZwn6ZnCf31tWzsbzFNM0wDQYJKoZIhvcNAQEL 3 | BQAwXzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQHDAhOZXcgWW9y 4 | azEOMAwGA1UECgwFRklOT1MxDDAKBgNVBAsMA0NUSTESMBAGA1UEAwwJbG9jYWxo 5 | b3N0MB4XDTI0MDUxMDEyNTQ1NFoXDTM0MDUwODEyNTQ1NFowXzELMAkGA1UEBhMC 6 | VVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQHDAhOZXcgWW9yazEOMAwGA1UECgwFRklO 7 | T1MxDDAKBgNVBAsMA0NUSTESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG 8 | 9w0BAQEFAAOCAg8AMIICCgKCAgEAssWStMgVaIWJBgUH+bhjoIzbgDXBcxco4k0r 9 | xEft+YXMtDv5wFWGBFfmbS8o7aOj7ENvJBgDy4OOy6r5zOJH/K4ZpJSwCAzEKsy6 10 | K8ymN/ZvcnF24HsYy8j6t3QM59T1RfdZAHN35VaLm0qzZwI/sE4JDybdHN1rkkrZ 11 | 8+EAgGjgO1vBm4p0tlWYFgA4MPC/eKo8sT+8lmlr5vHKeDjr7ojW74SV8YB1RPma 12 | YiVIR82EBvKUaZnsYS6XZfa1B6WTqWENuMpTcandsYRBWvDITuGn2PgGyTmsEZpm 13 | QtUuVGq9PGnOIAlvEKmgbSGOe3/+FMDF7aRkLmlOtPh4EL9+xBc7eLHHExxWh5lb 14 | AIQO9gj3w56uhjqu5GK40qO5oD45u7Z9zAiboFGhXVk4tB0ncxVr3UdC5yDiadkl 15 | 8TNAsUhw8VK29+YHUZVshQ2U2sNOGgJ5RfKokgY/0DD3EknqepZlsgCqe/zxW1b3 16 | MaIwm+e9xQn7AF6NcZtYoEum396NpKuKdX5HbGU0JOWa+sJBEkIGYlOZaOqTlgaR 17 | J5LwfrHdBuKGzlp/ti0ZZIR4iPwphrDAMEfsQf1+7bPR3mzPJBMaOqdDY6uSorpD 18 | Y6/N30YRLoVi3I8tr6UIO3tWZMkUShF0ipIJmTzcJl9MZHdD5RYLhkNUD8w9SB4m 19 | NyFvPWkCAwEAAaNTMFEwHQYDVR0OBBYEFHJZpw5gy81W7ndg8p7YkU9QGznZMB8G 20 | A1UdIwQYMBaAFHJZpw5gy81W7ndg8p7YkU9QGznZMA8GA1UdEwEB/wQFMAMBAf8w 21 | DQYJKoZIhvcNAQELBQADggIBAJkr6SD9dvKnxtF9taS2nTdHjwpBVEmMDudujJya 22 | NL/L9BhFWKsuUynF3Z1T+B2q9O5x2T7m25f3/o6K6uArJQlgLuAP8/v33YqDOiX2 23 | pYIZkzFXb9aEl6lVQ9MbMTYHpWrEPzd3n0Rd9SmgVYiTSaj/vPVXeWAnnXFUVVRC 24 | r936t2JuHOyf39os3OL71ndPoHIBXOHVJM2PH7i1V1hl2sM6N/MtVpOagy7RT4tt 25 | 14NQSIeYWZGV2XCes9fYmlNlMIr6v1rs4VlDojR1Ska8wyeF0V4/+dAVjzUVqsu5 26 | MoviLOM5ULxQQNbShFMKTK+xB2DK2V/AqAlDPAhfwj/WTH40iueIz4/6XAqaDrwO 27 | knkXsG4hHnhR+UnSMozuD2dHD+JHrozXZ9NWonEgGzaBz9jhdpYtrhHwh3XrZDkC 28 | wP5wm8My+Py2NofYOo8YMqHZoHBHYQWYfBV6CU3VcYsg/OW8NCsz1Fm8GyWsFDvK 29 | NJFBY1K3N2fRyBVxBdYqPnVKHhXlIu0c7u8Gk881trqtcq5YXvA6HvPGtMODqiLg 30 | sMUlLt9o4q34s8QohlOD6FjWiDCyThxaMVMgo1kPiIb07iWTfyErPNu+DXx13fCp 31 | M21hYqRlIXqnO1Hwjfaj0H5P5CbOdXrRhhMOtazeVfQX3WuXkdzIhhlYXGRIPRXj 32 | +wvB 33 | -----END CERTIFICATE----- 34 | --------------------------------------------------------------------------------