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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ trunk check enable {linter}
| Ruby | [brakeman], [rubocop], [rufo], [semgrep], [standardrb] |
| Rust | [clippy], [rustfmt] |
| Scala | [scalafmt] |
| Security | [checkov], [dustilock], [nancy], [osv-scanner], [snyk], [tfsec], [trivy], [trufflehog], [terrascan] |
| Security | [checkov], [dustilock], [ggshield], [nancy], [osv-scanner], [snyk], [tfsec], [trivy], [trufflehog], [terrascan] |
| SQL | [sqlfluff], [sqlfmt], [sql-formatter], [squawk] |
| SVG | [svgo] |
| Swift | [stringslint], [swiftlint], [swiftformat] |
Expand Down Expand Up @@ -122,6 +122,7 @@ trunk check enable {linter}
[flake8]: https://trunk.io/linters/python/flake8
[git-diff-check]: https://git-scm.com/docs/git-diff
[gitleaks]: https://trunk.io/linters/security/gitleaks
[ggshield]: https://docs.gitguardian.com/ggshield-docs/reference/overview
[gofmt]: https://pkg.go.dev/cmd/gofmt
[gofumpt]: https://pkg.go.dev/mvdan.cc/gofumpt
[goimports]: https://pkg.go.dev/golang.org/x/tools/cmd/goimports
Expand Down
57 changes: 57 additions & 0 deletions linters/ggshield/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# ggshield

[GitGuardian CLI](https://docs.gitguardian.com/ggshield-docs/reference/overview) for detecting
hardcoded secrets in your codebase.

## Setup

### Authentication

ggshield requires authentication to run. You can authenticate in one of two ways:

1. **Automatic authentication** (recommended for local development):

```bash
ggshield auth login
```

This opens a browser window for you to log in to your GitGuardian account.

2. **Environment variable** (recommended for CI/CD):
```bash
export GITGUARDIAN_API_KEY=your_api_key
```
You can create a personal access token in your
[GitGuardian dashboard](https://dashboard.gitguardian.com/).

### Configuration

ggshield can be configured using:

- `.gitguardian.yaml` or `.gitguardian.yml`
- `.ggshield.yaml` or `.ggshield.yml`

See the [GitGuardian documentation](https://docs.gitguardian.com/ggshield-docs/configuration) for
configuration options.

## Usage

Enable ggshield in your repository:

```bash
trunk check enable ggshield
```

## Features

- Scans all files for over 450+ types of hardcoded secrets
- Supports custom exclusion patterns
- Integrates with GitGuardian dashboard for secret management
- Provides detailed remediation guidance

## Best Practices

- Use `--exclude` patterns to skip files unlikely to contain secrets
- Configure `.gitguardian.yaml` to customize scanning behavior
- Set `GITGUARDIAN_API_KEY` environment variable for CI/CD environments
- Review and remediate detected secrets promptly
9 changes: 9 additions & 0 deletions linters/ggshield/ggshield.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import path from "path";
import { customLinterCheckTest } from "tests";
import { TEST_DATA } from "tests/utils";

customLinterCheckTest({
linterName: "ggshield",
testName: "basic",
args: path.join(TEST_DATA, "basic.in.py"),
});
134 changes: 134 additions & 0 deletions linters/ggshield/ggshield_to_sarif.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#!/usr/bin/env python3

import json
import os
import sys


def to_result_sarif(path: str, line_number: int, rule_id: str, description: str):
return {
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": path,
},
"region": {
"startColumn": 0,
"startLine": line_number,
},
}
}
],
"message": {
"text": description,
},
"ruleId": rule_id,
}


def main(argv):
try:
ggshield_json = json.load(sys.stdin)
except json.JSONDecodeError:
# If no JSON output or empty output, return empty SARIF
sarif = {
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [{"results": []}],
}
print(json.dumps(sarif, indent=2))
return

results = []

# ggshield JSON structure can vary, handle different possible formats
# Common structure: {"entities_with_incidents": [...]} or {"results": [...]}
incidents = []

if "entities_with_incidents" in ggshield_json:
# Format: {"entities_with_incidents": [{"filename": "...", "incidents": [...]}]}
for entity in ggshield_json.get("entities_with_incidents", []):
filename = entity.get("filename", "")
for incident in entity.get("incidents", []):
incidents.append(
{
"path": filename,
"line": incident.get("line", 0),
"type": incident.get("type", "Secret"),
"match": incident.get("match", ""),
"index_start": incident.get("index_start", 0),
"index_end": incident.get("index_end", 0),
}
)
elif "results" in ggshield_json:
# Alternative format: {"results": [...]}
for result in ggshield_json.get("results", []):
filename = result.get("filename", result.get("path", ""))
for incident in result.get("incidents", []):
incidents.append(
{
"path": filename,
"line": incident.get("line", incident.get("line_number", 0)),
"type": incident.get(
"type", incident.get("detector_name", "Secret")
),
"match": incident.get("match", incident.get("secret", "")),
"index_start": incident.get("index_start", 0),
"index_end": incident.get("index_end", 0),
}
)
elif isinstance(ggshield_json, list):
# Format: [{...}, {...}]
for item in ggshield_json:
if "filename" in item or "path" in item:
filename = item.get("filename", item.get("path", ""))
for incident in item.get("incidents", []):
incidents.append(
{
"path": filename,
"line": incident.get("line", 0),
"type": incident.get("type", "Secret"),
"match": incident.get("match", ""),
"index_start": incident.get("index_start", 0),
"index_end": incident.get("index_end", 0),
}
)

# Process incidents and create SARIF results
for incident in incidents:
path = incident.get("path", "")
line_number = incident.get("line", 0)
rule_id = incident.get("type", "Secret")
match = incident.get("match", "")

# Create description
if match:
# Redact the secret for display
if len(match) > 20:
redacted = match[:10] + "..." + match[-5:]
else:
redacted = "***REDACTED***"
description = f"Secret detected ({rule_id}): {redacted}"
else:
description = f"Secret detected ({rule_id})"

# Normalize path to be relative to workspace
if path and os.path.isabs(path):
# Try to make it relative if possible
pass

results.append(to_result_sarif(path, line_number, rule_id, description))

sarif = {
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [{"results": results}],
}

print(json.dumps(sarif, indent=2))


if __name__ == "__main__":
main(sys.argv)
65 changes: 65 additions & 0 deletions linters/ggshield/plugin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
version: 0.1
downloads:
- name: ggshield
downloads:
- url: https://github.com/GitGuardian/ggshield/releases/download/v${version}/ggshield-${version}-arm64-apple-darwin.tar.gz
os: macos
cpu: arm_64
strip_components: 1
- url: https://github.com/GitGuardian/ggshield/releases/download/v${version}/ggshield-${version}-x86_64-apple-darwin.tar.gz
os: macos
cpu: x86_64
strip_components: 1
- url: https://github.com/GitGuardian/ggshield/releases/download/v${version}/ggshield-${version}-x86_64-unknown-linux-gnu.tar.gz
os: linux
cpu: x86_64
strip_components: 1
- url: https://github.com/GitGuardian/ggshield/releases/download/v${version}/ggshield-${version}-x86_64-pc-windows-msvc.zip
os: windows
cpu: x86_64
strip_components: 1
tools:
definitions:
- name: ggshield
download: ggshield
shims: [ggshield]
known_good_version: 1.45.0
health_checks:
- command: ggshield --version
parse_regex: ggshield, version ${semver}
lint:
definitions:
- name: ggshield
files: [ALL]
tools: [ggshield]
description: Detect and fix hardcoded secrets in your codebase
known_good_version: 1.45.0
suggest_if: files_present
commands:
- name: lint
output: sarif
run: ggshield secret scan path --recursive --yes ${target} --json
read_output_from: stdout
success_codes: [0, 1]
is_security: true
batch: true
cache_results: true
sandbox_type: copy_targets
parser:
runtime: python
run: python3 ${plugin}/linters/ggshield/ggshield_to_sarif.py
direct_configs:
- .gitguardian.yaml
- .gitguardian.yml
- .ggshield.yaml
- .ggshield.yml
environment:
- name: GITGUARDIAN_API_KEY
optional: true
value: ${env.GITGUARDIAN_API_KEY}
- name: GITGUARDIAN_INSTANCE
optional: true
value: ${env.GITGUARDIAN_INSTANCE}
version_command:
parse_regex: ggshield, version ${semver}
run: ggshield --version
22 changes: 22 additions & 0 deletions linters/ggshield/test_data/basic.in.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Test file with various types of secrets that ggshield should detect

# AWS Access Key
aws_access_key_id = "AKIAIO5FODNN7EXAMPLE"

# AWS Token
aws_token = "AKIALALEMEL33243OLIA"

# Private Key
private_key = """-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACA8YWKYztuuvxUIMomc3zv0OdXCT57Cc2cRYu3TMbX9XAAAAJDiKO3C4ijt
wgAAAAtzc2gtZWQyNTUxOQAAACA8YWKYztuuvxUIMomc3zv0OdXCT57Cc2cRYu3TMbX9XA
AAAECzmj8DGxg5YHtBK4AmBttMXDQHsPAaCyYHQjJ4YujRBTxhYpjO266/FQgyiZzfO/Q5
1cJPnsJzZxFi7dMxtf1cAAAADHJvb3RAZGV2aG9zdAE=
-----END OPENSSH PRIVATE KEY-----"""

# GitHub Token (example)
github_token = "ghp_1234567890abcdefghijklmnopqrstuvwxyz"

# Generic API Key (test data - clearly fake)
api_key = "sk_test_FAKE_1234567890abcdefghijklmnopqrstuvwxyz_TEST_ONLY"
51 changes: 51 additions & 0 deletions linters/ggshield/test_data/ggshield_v1.45.0_basic.check.shot
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Testing linter ggshield test basic 1`] = `
{
"issues": [
{
"code": "Generic-High-Entropy-Secret",
"file": "test_data/basic.in.py",
"isSecurity": true,
"issueClass": "ISSUE_CLASS_EXISTING",
"level": "LEVEL_HIGH",
"linter": "ggshield",
"message": "Secret detected (Generic High Entropy Secret)",
"targetType": "ALL",
},
{
"code": "OpenSSH-Private-Key",
"file": "test_data/basic.in.py",
"isSecurity": true,
"issueClass": "ISSUE_CLASS_EXISTING",
"level": "LEVEL_HIGH",
"linter": "ggshield",
"message": "Secret detected (OpenSSH Private Key)",
"targetType": "ALL",
},
],
"lintActions": [
{
"command": "lint",
"fileGroupName": "ALL",
"linter": "ggshield",
"paths": [
"test_data/basic.in.py",
],
"verb": "TRUNK_VERB_CHECK",
},
{
"command": "lint",
"fileGroupName": "ALL",
"linter": "ggshield",
"paths": [
"test_data/basic.in.py",
],
"upstream": true,
"verb": "TRUNK_VERB_CHECK",
},
],
"taskFailures": [],
"unformattedFiles": [],
}
`;
1 change: 1 addition & 0 deletions tests/repo_tests/config_check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ describe("Global config health check", () => {
"clippy",
"cue-fmt",
"dotenv-linter",
"ggshield",
"git-diff-check",
"gofmt",
"golangci-lint2",
Expand Down
Loading