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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# GitArmor
GitArmor is a TypeScript Node.js project that functions as both a GitHub Action and CLI tool for security assessments of GitHub repositories and organizations. It transforms DevOps platform security policies into YAML policies as code and runs checks against GitHub environments.

Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.

## Working Effectively

### Bootstrap and Build
- Check Node.js version (requires v18+): `node --version`
- Install dependencies: `npm install` -- takes ~5 seconds. NEVER CANCEL. Set timeout to 60+ seconds.
- Build the project: `npm run build` -- takes ~11 seconds. NEVER CANCEL. Set timeout to 60+ seconds.
- This runs: `npm run clean && tsc && ncc build ./src/main.ts -o dist --license licenses.txt`
- Compiles TypeScript source in `src/` to JavaScript
- Bundles everything into `dist/index.js` using ncc for GitHub Action distribution

### Code Quality and Formatting
- Check code formatting: `npm run prettier:check` -- takes ~1 second
- Fix code formatting: `npm run prettier:write` -- takes ~1 second
- Check security vulnerabilities: `npm audit` -- takes ~1 second
- **DO NOT run `npm audit fix`** -- it introduces TypeScript compilation errors due to dependency version conflicts

### Configuration and Setup
- Copy `.env.sample` to `.env` and configure:
```bash
cp .env.sample .env
```
- Required environment variables in `.env`:
- `TOKEN`: GitHub Personal Access Token with repo:admin and org:admin permissions
- `LEVEL`: `repository_only`, `organization_only`, or `organization_and_repository`
- `REPO`: Repository name (when using repository-level checks)
- `ORG`: Organization name
- `DEBUG`: `true` or `false`
- `POLICY_DIR`: Path to policies directory (default: `./policies`)

### Running the Application
- CLI mode: `npm run start` -- builds and runs, takes ~11 seconds + runtime. NEVER CANCEL. Set timeout to 300+ seconds.
- Direct execution after build: `node ./dist/index.js`

## Validation

### Manual Testing Scenarios
ALWAYS test these scenarios after making changes:

1. **Build Validation**:
```bash
npm install
npm run build
```
- Build should complete successfully in ~11 seconds
- Should generate `dist/index.js` and `dist/licenses.txt`

2. **Code Quality Validation**:
```bash
npm run prettier:check
npm run prettier:write
npm audit
```
- Prettier should pass or auto-fix formatting issues
- npm audit will show 4 moderate vulnerabilities (known issue, do not fix)

3. **CLI Functionality Test**:
```bash
# Create test .env with fake token
echo "TOKEN=fake_token_for_testing
LEVEL=repository_only
REPO=gitarmor
ORG=dcodx
DEBUG=true
POLICY_DIR=./policies" > .env

npm run start
```
- Should start successfully, load policies, and fail with "Blocked by DNS monitoring proxy" or authentication error (expected with fake token)
- Should show GitArmor banner and debug information

4. **Policy Loading Test**:
- Verify `policies/repository.yml` and `policies/organization.yml` exist and are valid YAML
- Application should load policies without errors during startup

### GitHub Action Testing
- The project includes example workflows in `.github/workflows/`:
- `gitarmor-on-demand.yml` - Manual trigger workflow
- `gitarmor-scheduled.yml` - Scheduled daily workflow
- Action defined in `action.yml` with Node 20 runtime
- Outputs: `check-results-json` and `check-results-text`

## Common Tasks

### Repository Structure
```
ls -la
.env.sample
.github/ # GitHub workflows and documentation
.gitignore
LICENSE
README.md
action.yml # GitHub Action definition
dist/ # Built artifacts (created by npm run build)
package.json # Dependencies and scripts
policies/ # YAML policy definitions
src/ # TypeScript source code
tsconfig.json # TypeScript configuration
```

### Key Source Directories
```
ls -la src/
evaluators/ # Policy evaluation logic
github/ # GitHub API interaction
main.ts # Application entry point
reporting/ # Report generation
types/ # TypeScript type definitions
utils/ # Utility functions (logger, input parsing, etc.)
```

### Policy Files
```
ls -la policies/
organization.yml # Organization-level security policies
organization.readme.md
organization.threats.md
repository.yml # Repository-level security policies
repository.readme.md
repository.threats.md
```

### Package.json Scripts
- `npm run test` -- Currently not implemented (exits with error)
- `npm run clean` -- Removes dist directory contents
- `npm run build` -- Full build: clean + compile + bundle
- `npm run prettier:write` -- Auto-fix code formatting
- `npm run prettier:check` -- Check code formatting
- `npm run start` -- Build and run CLI application

## Important Notes

### Critical Warnings
- **NEVER CANCEL** any build or test command. Set timeouts of 60+ seconds minimum.
- **DO NOT run `npm audit fix`** - it breaks the build due to dependency version conflicts.
- Always run `npm run prettier:write` before committing to ensure consistent formatting.
- The `.env` file contains sensitive tokens - it's already in `.gitignore`.

### Known Issues
- 4 moderate security vulnerabilities in dependencies (@octokit packages and undici)
- No test suite currently implemented
- TypeScript import paths are case-sensitive (use lowercase: `logger`, `input`)

### Working with GitHub API
- Requires valid GitHub token with appropriate permissions
- Tool makes extensive API calls - organization_and_repository level may hit rate limits
- Supports repository-only, organization-only, or both levels of analysis
- Generates reports in both JSON and Markdown formats (`output-report.json`, `output-report.md`)

### Development Tips
- Check `src/utils/input.ts` for understanding input parameter parsing
- Policy definitions in `policies/` directory control what security checks are performed
- Logger configuration in `src/utils/logger.ts` - set DEBUG=true for verbose output
- Main application logic in `src/main.ts` coordinates policy evaluation and reporting
22 changes: 11 additions & 11 deletions dist/evaluators/OrgPolicyEvaluator.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.OrgPolicyEvaluator = void 0;
const Logger_1 = require("../utils/Logger");
const logger_1 = require("../utils/logger");
const OrgGHASChecks_1 = require("./organization/OrgGHASChecks");
const OrgAuthenticationChecks_1 = require("./organization/OrgAuthenticationChecks");
const OrgCustomRolesChecks_1 = require("./organization/OrgCustomRolesChecks");
Expand All @@ -19,43 +19,43 @@ class OrgPolicyEvaluator {
}
// This method evaluates the policy for the repository
async evaluatePolicy() {
Logger_1.logger.info("Running checks for organization policy against: " +
logger_1.logger.info("Running checks for organization policy against: " +
this.organization.name);
Logger_1.logger.debug("Organization policy for org: " + this.organization.name);
logger_1.logger.debug("Organization policy for org: " + this.organization.name);
// Get the organization data from the API
this.organization.data = await (0, Organization_1.getOrganization)(this.organization.name);
// Check MemberPrivileges policy rule
if (this.policy.member_privileges) {
const member_privileges = await new PrivilegesChecks_1.PrivilegesChecks(this.organization, this.policy).checkPrivileges();
Logger_1.logger.debug(`Member privileges results: ${JSON.stringify(member_privileges)}`);
logger_1.logger.debug(`Member privileges results: ${JSON.stringify(member_privileges)}`);
this.orgCheckResults.push(member_privileges);
}
// Check org level GHAS settings
if (this.policy.advanced_security) {
const ghas_checks = await new OrgGHASChecks_1.OrgGHASChecks(this.policy, this.organization, this.organization.data).evaluate();
Logger_1.logger.debug(`Org GHAS results: ${JSON.stringify(ghas_checks)}`);
logger_1.logger.debug(`Org GHAS results: ${JSON.stringify(ghas_checks)}`);
this.orgCheckResults.push(ghas_checks);
}
// check authentication settings
if (this.policy.authentication) {
const authentication_checks = await new OrgAuthenticationChecks_1.OrgAuthenticationChecks(this.policy, this.organization, this.organization.data).evaluate();
Logger_1.logger.debug(`Org Authentication results: ${JSON.stringify(authentication_checks)}`);
logger_1.logger.debug(`Org Authentication results: ${JSON.stringify(authentication_checks)}`);
this.orgCheckResults.push(authentication_checks);
}
// check custom repository roles
if (this.policy.custom_roles) {
const custom_roles_checks = await new OrgCustomRolesChecks_1.OrgCustomRolesChecks(this.policy, this.organization, this.organization.data).evaluate();
Logger_1.logger.debug(`Org Custom Roles results: ${JSON.stringify(custom_roles_checks)}`);
logger_1.logger.debug(`Org Custom Roles results: ${JSON.stringify(custom_roles_checks)}`);
this.orgCheckResults.push(custom_roles_checks);
}
}
printCheckResults() {
Logger_1.logger.info("------------------------------------------------------------------------");
Logger_1.logger.info(`Organization policy results - ${this.organization.name}:`);
Logger_1.logger.info("------------------------------------------------------------------------");
logger_1.logger.info("------------------------------------------------------------------------");
logger_1.logger.info(`Organization policy results - ${this.organization.name}:`);
logger_1.logger.info("------------------------------------------------------------------------");
this.orgCheckResults.forEach((checkResult) => {
const emoji = checkResult.pass === null ? "😐" : checkResult.pass ? "βœ…" : "❌";
Logger_1.logger.info(`[${emoji}] Check: ${checkResult.name} - Pass: ${checkResult.pass} \n${JSON.stringify(checkResult.data, null, 3)}`);
logger_1.logger.info(`[${emoji}] Check: ${checkResult.name} - Pass: ${checkResult.pass} \n${JSON.stringify(checkResult.data, null, 3)}`);
});
}
getCheckResults() {
Expand Down
33 changes: 17 additions & 16 deletions dist/evaluators/RepoPolicyEvaluator.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RepoPolicyEvaluator = void 0;
const Logger_1 = require("./../utils/Logger");
const logger_1 = require("./../utils/logger");
const BranchProtectionChecks_1 = require("./repository/BranchProtectionChecks");
const GHASChecks_1 = require("./repository/GHASChecks");
const Repositories_1 = require("../github/Repositories");
Expand All @@ -27,64 +27,65 @@ class RepoPolicyEvaluator {
async evaluatePolicy() {
// Get the repository data from the API
this.repositoryData = await (0, Repositories_1.getRepository)(this.repository.owner, this.repository.name);
Logger_1.logger.debug("Repository policy for repo: " + this.repository.name);
logger_1.logger.debug("Repository policy for repo: " + this.repository.name);
// Check the branch protection policy rule
if (this.policy.protected_branches && this.policy.protected_branches.length > 0) {
if (this.policy.protected_branches &&
this.policy.protected_branches.length > 0) {
const branch_protection = new BranchProtectionChecks_1.BranchProtectionChecks(this.policy, this.repository);
const branch_protection_results = await branch_protection.checkBranchProtection();
Logger_1.logger.debug(`Branch protection rule results: ${JSON.stringify(branch_protection_results, null, 2)}`);
logger_1.logger.debug(`Branch protection rule results: ${JSON.stringify(branch_protection_results, null, 2)}`);
this.repositoryCheckResults.push(branch_protection_results);
// Check if require pull request before merging is enabled for the protected branches
const branch_protection_pull_request_results = await branch_protection.checkRequirePullRequest();
Logger_1.logger.debug(`Branch protection pull requests rule results: ${JSON.stringify(branch_protection_pull_request_results, null, 2)}`);
logger_1.logger.debug(`Branch protection pull requests rule results: ${JSON.stringify(branch_protection_pull_request_results, null, 2)}`);
this.repositoryCheckResults.push(branch_protection_pull_request_results);
}
// Check the files exist policy rule
if (this.policy.file_exists && this.policy.file_exists.length > 0) {
const files_exist = await new FilesExistChecks_1.FilesExistChecks(this.repository, this.policy).checkFilesExist();
Logger_1.logger.debug(`Files exists results: ${JSON.stringify(files_exist)}`);
logger_1.logger.debug(`Files exists results: ${JSON.stringify(files_exist)}`);
this.repositoryCheckResults.push(files_exist);
}
//Run the GHAS checks
if (this.policy.advanced_security) {
const ghas_checks = await new GHASChecks_1.GHASChecks(this.policy, this.repository, this.repositoryData).evaluate();
Logger_1.logger.debug(`GHAS results: ${JSON.stringify(ghas_checks)}`);
logger_1.logger.debug(`GHAS results: ${JSON.stringify(ghas_checks)}`);
this.repositoryCheckResults.push(ghas_checks);
}
//Run Actions checks
if (this.policy.allowed_actions) {
const actions_checks = await new ActionsChecks_1.ActionsChecks(this.policy, this.repository).checkActionsPermissions();
Logger_1.logger.debug(`Action checks results: ${JSON.stringify(actions_checks)}`);
logger_1.logger.debug(`Action checks results: ${JSON.stringify(actions_checks)}`);
this.repositoryCheckResults.push(actions_checks);
}
//Run workflow checks
if (this.policy.workflows) {
const workflow_checks = await new WorkflowsChecks_1.WorkflowsChecks(this.policy, this.repository).checkWorkflowsDefaultPermissions();
Logger_1.logger.debug(`Workflow checks results: ${JSON.stringify(workflow_checks)}`);
logger_1.logger.debug(`Workflow checks results: ${JSON.stringify(workflow_checks)}`);
//This check only applies to private and internal repositories
const workflow_access_checks = await new WorkflowsChecks_1.WorkflowsChecks(this.policy, this.repository).checkWorkflowsAccessPermissions();
Logger_1.logger.debug(`Workflow access checks results: ${JSON.stringify(workflow_access_checks)}`);
logger_1.logger.debug(`Workflow access checks results: ${JSON.stringify(workflow_access_checks)}`);
}
//Run runner checks
if (this.policy.runners) {
const runner_checks = await new RunnersChecks_1.RunnersChecks(this.policy, this.repository).checkRunnersPermissions();
Logger_1.logger.debug(`Runner checks results: ${JSON.stringify(runner_checks)}`);
logger_1.logger.debug(`Runner checks results: ${JSON.stringify(runner_checks)}`);
this.repositoryCheckResults.push(runner_checks);
}
if (this.policy.webhooks) {
const webhook_checks = await new WebHooksChecks_1.WebHooksChecks(this.policy, this.repository).checkWebHooks();
Logger_1.logger.debug(`Webhook checks results: ${JSON.stringify(webhook_checks)}`);
logger_1.logger.debug(`Webhook checks results: ${JSON.stringify(webhook_checks)}`);
this.repositoryCheckResults.push(webhook_checks);
}
}
// Run webhook checks
printCheckResults() {
Logger_1.logger.info("------------------------------------------------------------------------");
Logger_1.logger.info(`Repository policy results - ${this.getFullRepositoryName()}:`);
Logger_1.logger.info("------------------------------------------------------------------------");
logger_1.logger.info("------------------------------------------------------------------------");
logger_1.logger.info(`Repository policy results - ${this.getFullRepositoryName()}:`);
logger_1.logger.info("------------------------------------------------------------------------");
this.repositoryCheckResults.forEach((checkResult) => {
const emoji = checkResult.pass === null ? "😐" : checkResult.pass ? "βœ…" : "❌";
Logger_1.logger.info(`[${emoji}] Check: ${checkResult.name} - Pass: ${checkResult.pass} \n${JSON.stringify(checkResult.data, null, 3)}`);
logger_1.logger.info(`[${emoji}] Check: ${checkResult.name} - Pass: ${checkResult.pass} \n${JSON.stringify(checkResult.data, null, 3)}`);
});
}
getFullRepositoryName() {
Expand Down
6 changes: 3 additions & 3 deletions dist/evaluators/repository/ActionsChecks.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.ActionsChecks = void 0;
const Actions_1 = require("../../github/Actions");
const Logger_1 = require("../../utils/Logger");
const logger_1 = require("../../utils/logger");
const POLICY_VALUES = ["none", "all", "local_only", "selected"];
class ActionsChecks {
policy;
Expand All @@ -22,7 +22,7 @@ class ActionsChecks {
switch (actionsPermissionsPolicy) {
case "selected":
if (!this.policy.allowed_actions.selected.patterns_allowed) {
Logger_1.logger.error("error: the policy (.yml) should have the list of patterns_allowed when permission is 'selected'");
logger_1.logger.error("error: the policy (.yml) should have the list of patterns_allowed when permission is 'selected'");
return this.createResult(false, actionsPermissionsAllowedActions, actionsPermissionsPolicy);
}
if (actionsPermissionsAllowedActions !== "selected")
Expand All @@ -42,7 +42,7 @@ class ActionsChecks {
case "none":
return this.createResult(actionsPermissionsPolicy === actionsPermissionsAllowedActions, actionsPermissionsAllowedActions, actionsPermissionsPolicy);
default:
Logger_1.logger.error(`error: invalid policy value '${actionsPermissionsPolicy}'. It should be one of ${POLICY_VALUES.join(", ")}.`);
logger_1.logger.error(`error: invalid policy value '${actionsPermissionsPolicy}'. It should be one of ${POLICY_VALUES.join(", ")}.`);
}
}
createResult(actions_permissions, github_allowed_actions, policy_allowed_actions) {
Expand Down
6 changes: 3 additions & 3 deletions dist/evaluators/repository/BranchProtectionChecks.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.BranchProtectionChecks = void 0;
const Repositories_1 = require("../../github/Repositories");
const Logger_1 = require("../../utils/Logger");
const logger_1 = require("../../utils/logger");
class BranchProtectionChecks {
policy;
repository;
Expand Down Expand Up @@ -97,7 +97,7 @@ class BranchProtectionChecks {
}
getProtectedBranchesToCheck(branchesToCheck, protectedBranches) {
const branchesAvailable = branchesToCheck.filter((branch) => protectedBranches.map((branch) => branch.name).includes(branch));
Logger_1.logger.debug("Only these branches will be checked against branch protection rules: " +
logger_1.logger.debug("Only these branches will be checked against branch protection rules: " +
branchesAvailable);
return branchesAvailable;
}
Expand Down Expand Up @@ -125,7 +125,7 @@ class BranchProtectionChecks {
}
catch (error) {
// If the branch is protected but no rules are set.
Logger_1.logger.debug(`Exception: ${error}`);
logger_1.logger.debug(`Exception: ${error}`);
results[branch] = {
error: "No branch protection rules set for this branch",
};
Expand Down
Loading