Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
7c87639
dummy commit, check PR #319 description
cx-leonardo-fontes Aug 8, 2025
b18758b
Merge branch 'master' of https://github.com/Checkmarx/2ms
cx-leonardo-fontes Aug 22, 2025
071dbe0
Merge branch 'master' of https://github.com/Checkmarx/2ms
cx-leonardo-fontes Aug 29, 2025
d245104
Merge branch 'master' of https://github.com/Checkmarx/2ms
cx-leonardo-fontes Sep 1, 2025
3139b02
Merge branch 'master' of https://github.com/Checkmarx/2ms
cx-leonardo-fontes Sep 1, 2025
f666f3d
revamp confluence scan using their rest api v2
cx-leonardo-fontes Sep 5, 2025
011ab0b
Add improvements to confluence logic, update tests and documentation
cx-leonardo-fontes Sep 22, 2025
5912c6a
Merge branch 'master' of https://github.com/Checkmarx/2ms
cx-leonardo-fontes Sep 22, 2025
ada4d3d
fix conflict
cx-leonardo-fontes Sep 22, 2025
590e5f6
update readme
cx-leonardo-fontes Sep 22, 2025
1084f37
fix linter issues
cx-leonardo-fontes Sep 22, 2025
ed76e8d
fix test
cx-leonardo-fontes Sep 22, 2025
1e85d8c
remove missplaced test
cx-leonardo-fontes Sep 22, 2025
9717de5
load one page at a time into the memory
cx-leonardo-fontes Oct 2, 2025
70f94bb
remove redundant assert in confluence_client_test.go
cx-leonardo-fontes Oct 2, 2025
56c84a9
Merge branch 'confluence-revamp' of https://github.com/Checkmarx/2ms …
cx-leonardo-fontes Oct 6, 2025
e8c1c21
update stream pages logic to use experimental json v2
cx-leonardo-fontes Oct 14, 2025
981a0b7
update flags to allow classic and scoped api tokens for confluence
cx-leonardo-fontes Oct 15, 2025
dc5d90c
add chunking, improvements and update unit tests
cx-leonardo-fontes Oct 17, 2025
e5e1393
update dockerfile image and linter version from makefile
cx-leonardo-fontes Oct 17, 2025
3a1c7fb
add jsonv2 experiment in dockerfile
cx-leonardo-fontes Oct 17, 2025
f8776cc
add jsonv2 experiment for go test ocurrences in ci
cx-leonardo-fontes Oct 17, 2025
f5ebff0
update golangci lint version in ci
cx-leonardo-fontes Oct 17, 2025
ae935e5
update to use jsonv2 experiment in golangci lint
cx-leonardo-fontes Oct 17, 2025
2aa693e
fix linter issues
cx-leonardo-fontes Oct 17, 2025
f8faf1d
Merge branch 'master' into confluence-revamp
cx-leonardo-fontes Oct 17, 2025
ed20a16
add jsonv2 experiment env in more actions
cx-leonardo-fontes Oct 20, 2025
e747b3d
Merge branch 'confluence-revamp' of https://github.com/Checkmarx/2ms …
cx-leonardo-fontes Oct 20, 2025
77e33d8
small changes in unit tests
cx-leonardo-fontes Oct 20, 2025
7fff055
use assert in unit test instead
cx-leonardo-fontes Oct 20, 2025
383d79a
Merge branch 'master' into confluence-revamp
cx-leonardo-fontes Oct 20, 2025
7e97db5
added constructor for confluence plugin and isolate better variables …
cx-leonardo-fontes Oct 20, 2025
22b342f
Merge branch 'confluence-revamp' of https://github.com/Checkmarx/2ms …
cx-leonardo-fontes Oct 20, 2025
742c464
fix linter
cx-leonardo-fontes Oct 21, 2025
e131161
add sentinel errors and move common logic to walkPaginated
cx-leonardo-fontes Oct 21, 2025
2f4cba7
add unit test for discoverCloudID and update other tests
cx-leonardo-fontes Oct 21, 2025
c01914b
update to use chunker mock in tests and exactly values for number of …
cx-leonardo-fontes Oct 21, 2025
6cf6be0
fix test
cx-leonardo-fontes Oct 21, 2025
b0887ca
Update tests to use ErrorIs and increase coverage for some tests
cx-leonardo-fontes Oct 22, 2025
6002909
remove NA tests
cx-leonardo-fontes Oct 22, 2025
5d592ce
Update documentation
cx-leonardo-fontes Oct 23, 2025
575b989
update flaky test
cx-leonardo-fontes Oct 23, 2025
ba75be7
rename token types
cx-leonardo-fontes Oct 23, 2025
811f489
ignore fp from linter
cx-leonardo-fontes Oct 23, 2025
34627cc
fix race condition
cx-leonardo-fontes Oct 23, 2025
90f3dae
Update readme to include GOEXPERIMENT jsonv2 for building from source
cx-leonardo-fontes Oct 27, 2025
21cc37e
update to correct version requirement in readme
cx-leonardo-fontes Oct 27, 2025
4c6948d
Revert "Update readme to include GOEXPERIMENT jsonv2 for building fro…
cx-leonardo-fontes Oct 28, 2025
03c298a
revert jsonv2 experiment for now
cx-leonardo-fontes Oct 28, 2025
650d7b1
fix linter issue
cx-leonardo-fontes Oct 28, 2025
44c2b2b
update workflows to use the go version of the go mod
cx-leonardo-fontes Oct 28, 2025
7b144fd
fix tests
cx-leonardo-fontes Oct 28, 2025
7a187bf
Merge branch 'master' into confluence-revamp
cx-leonardo-fontes Nov 3, 2025
6e83095
update dockerfile version
cx-leonardo-fontes Nov 3, 2025
79bfc43
Merge branch 'confluence-revamp' of https://github.com/Checkmarx/2ms …
cx-leonardo-fontes Nov 3, 2025
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
6 changes: 2 additions & 4 deletions .github/workflows/codecov.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ on:
jobs:
run:
runs-on: ubuntu-latest
env:
go-version: 'stable'

steps:
- name: Checkout code
Expand All @@ -21,11 +19,11 @@ jobs:
- name: Set up Go
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
with:
go-version: ${{ env.go-version }}
go-version-file: go.mod
env:
GOPROXY: direct
GONOSUMDB: "*"
GOPRIVATE: https://github.com/CheckmarxDev/ # Add your private organization url here
GOPRIVATE: https://github.com/CheckmarxDev/

- name: Install dependencies
run: go install golang.org/x/tools/cmd/cover@latest
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/new-rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ jobs:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
with:
go-version: "^1.22"
go-version-file: go.mod
- name: Check Gitleaks new rules
run: go run .ci/check_new_rules.go
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:

- uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
with:
go-version: "^1.22"
go-version-file: go.mod

- name: Go Mod Tidy
run: go mod tidy
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/validate-readme.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
with:
go-version: "^1.22"
go-version-file: go.mod

- name: update README
run: ./.ci/update-readme.sh
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# and "Missing User Instruction" since 2ms container is stopped after scan

# Builder image
FROM checkmarx/go:1.24.4-r0-ae7309142bb6bd@sha256:ae7309142bb6bd82e0272c3624ec53c0c68d855f6b63e985c5caaff5c1705644 AS builder
FROM checkmarx/go:1.25.3-r0-b47cbbc1194cd0@sha256:b47cbbc1194cd0d801fe7739fca12091d610117b0d30c32b52fc900217a0821a AS builder

WORKDIR /app

Expand Down
58 changes: 42 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ Usage:
2ms [command]

Scan Commands
confluence Scan Confluence server
confluence Scan Confluence Cloud
discord Scan Discord server
filesystem Scan local folder
git Scan local Git repository
Expand Down Expand Up @@ -274,30 +274,56 @@ This command is used to scan a [Confluence](https://www.atlassian.com/software/c
2ms confluence <URL> [flags]
```

| Flag | Type | Default | Description |
| ------------ | ----- | ------------------------------ | -------------------------------------------------------------------------------- |
| `<url>` | string | - | Confluence instance URL in the following format: `https://<company id>.atlassian.net/wiki` |
| `--history` | - | Doesn't scan history revisions | Scans pages history revisions |
| `--spaces` | string | all spaces | The names or IDs of the Confluence spaces to scan |
| `--token` | string | - | The Confluence API token for authentication |
| `--username` | string | - | Confluence user name or email for authentication |
| Flag | Type | Default | Description |
| --------------- | ----------- | ------- |----------------------------------------------------------------------------------|
| `--space-keys` | string list | (all) | Comma-separated list of space **keys** to scan. |
| `--space-ids` | string list | (all) | Comma-separated list of space **IDs** to scan. |
| `--page-ids` | string list | (all) | Comma-separated list of **page IDs** to scan. |
| `--history` | bool | `false` | Also scan **all versions** of each page (page history). |
| `--username` | string | | Confluence username/email (used for HTTP Basic Auth). |
| `--token-type` | string | | Token type for Confluence API. Accepted values: `api-token`, `scoped-api-token`. |
| `--token-value` | string | | The API token value. **Required** when `--token-type` is set. |

For example:
#### Authentication
- To scan **private spaces**, provide `--username`, `--token-type` and `--token-value` (API token).
- How to create a Confluence API token: https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/

#### Examples

- Scan **all public pages** (no auth):
```bash
2ms confluence https://<company id>.atlassian.net/wiki
```

- To scan public spaces:
- Scan **private pages with an api token** (requires auth):
```bash
2ms confluence https://<company id>.atlassian.net/wiki --username <USERNAME> --token-type api-token --token-value <API_TOKEN>
```

- Scan **private pages with a scoped api token** (requires auth):
```bash
2ms confluence https://checkmarx.atlassian.net/wiki --spaces secrets
2ms confluence https://<company id>.atlassian.net/wiki --username <USERNAME> --token-type scoped-api-token --token-value <API_TOKEN>
```
💡 [The `secrets` Confluence site](https://checkmarx.atlassian.net/wiki/spaces/secrets) purposely created with plain example secrets as a test subject for this demo

- To scan private spaces, authentication is required
- Scan specific **spaces by key**:
```bash
2ms confluence <URL> --username <USERNAME> --token <API_TOKEN> --spaces <SPACES>
2ms confluence https://<company id>.atlassian.net/wiki --space-keys Key1,Key2
```
[How to get a Confluence API token](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/).

[![asciicast](https://asciinema.org/a/607179.svg)](https://asciinema.org/a/607179)
- Scan specific **spaces by ID**:
```bash
2ms confluence https://<company id>.atlassian.net/wiki --space-ids 1234567890,9876543210
```

- Scan specific **pages by ID**:
```bash
2ms confluence https://<company id>.atlassian.net/wiki --page-ids 11223344556,99887766554
```

- Include **page history** (all revisions):
```bash
2ms confluence https://<company id>.atlassian.net/wiki --history
```

### Paligo

Expand Down
10 changes: 9 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ var configFilePath string
var vConfig = viper.New()

var allPlugins = []plugins.IPlugin{
&plugins.ConfluencePlugin{},
plugins.NewConfluencePlugin(),
&plugins.DiscordPlugin{},
&plugins.FileSystemPlugin{},
&plugins.SlackPlugin{},
Expand Down Expand Up @@ -112,9 +112,17 @@ func Execute() (int, error) {
}
subCommand.GroupID = group

pluginPreRun := subCommand.PreRunE
// Capture plugin name for closure
pluginName := plugin.GetName()
subCommand.PreRunE = func(cmd *cobra.Command, args []string) error {
// run plugin's own PreRunE (if any)
if pluginPreRun != nil {
if err := pluginPreRun(cmd, args); err != nil {
return err
}
}
// run engine-level PreRunE
return preRun(pluginName, engineInstance, cmd, args)
}
subCommand.PostRunE = func(cmd *cobra.Command, args []string) error {
Expand Down
24 changes: 21 additions & 3 deletions engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ func (e *Engine) detectSecrets(
if buildErr != nil {
return fmt.Errorf("failed to build secret: %w", buildErr)
}
if !isSecretIgnored(secret, e.ignoredIds, e.allowedValues) {
if !isSecretIgnored(secret, e.ignoredIds, e.allowedValues, value.Line, value.Match, pluginName) {
secrets <- secret
} else {
log.Debug().Msgf("Secret %s was ignored", secret.ID)
Expand Down Expand Up @@ -575,13 +575,15 @@ func getStartAndEndLines(
return startLine, endLine, nil
}

func isSecretIgnored(secret *secrets.Secret, ignoredIds, allowedValues *[]string) bool {
func isSecretIgnored(secret *secrets.Secret, ignoredIds, allowedValues *[]string, secretLine, secretMatch, pluginName string) bool {
for _, allowedValue := range *allowedValues {
if secret.Value == allowedValue {
return true
}
}

if pluginName == "confluence" && isSecretFromConfluenceResourceIdentifier(secret.RuleID, secretLine, secretMatch) {
return true
}
return slices.Contains(*ignoredIds, secret.ID)
}

Expand Down Expand Up @@ -740,3 +742,19 @@ func (e *Engine) Scan(pluginName string) {
func (e *Engine) Wait() {
e.wg.Wait()
}

// isSecretFromConfluenceResourceIdentifier reports whether a regex match found in a line
// actually belongs to Confluence Storage Format metadata (the `ri:` namespace) rather than
// real user content. This lets us ignore false-positives that cannot be suppressed via the
// generic-api-key rule allow-list.
func isSecretFromConfluenceResourceIdentifier(secretRuleID, secretLine, secretMatch string) bool {
if secretRuleID != rules.GenericApiKeyID || secretLine == "" || secretMatch == "" {
return false
}

q := regexp.QuoteMeta(secretMatch)

pat := `<[^>]*\sri:` + q + `[^>]*>`
re := regexp.MustCompile(pat)
return re.MatchString(secretLine)
}
114 changes: 114 additions & 0 deletions engine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,120 @@ func TestGetFindingId(t *testing.T) {
})
}

func TestIsSecretFromConfluenceResourceIdentifier(t *testing.T) {
tests := []struct {
name string
ruleID string
line string
match string
want bool
}{
{
name: "matches ri:secret attribute with quoted value",
ruleID: rules.GenericApiKeyID,
line: `<ri:attachment ri:secret="12345" />`,
match: `secret="12345"`,
want: true,
},
{
name: "matches with extra whitespace and self-closing tag",
ruleID: rules.GenericApiKeyID,
line: `<ri:attachment ri:secret="12345"/>`,
match: `secret="12345"`,
want: true,
},
{
name: "no match when value format differs (expects exact literal)",
ruleID: rules.GenericApiKeyID,
line: `<ri:attachment ri:secret="12345" />`,
match: `secret=12345`,
want: false,
},
{
name: "no match when value appears in a different attribute",
ruleID: rules.GenericApiKeyID,
line: `<ri:attachment ri:filename="secret=12345" />`,
match: `secret=12345`,
want: false,
},
{
name: "no match when ri: prefixes the element name (not an attribute)",
ruleID: rules.GenericApiKeyID,
line: `<ri:secret value="x">`,
match: `secret`,
want: false,
},
{
name: "no match when text is outside any tag",
ruleID: rules.GenericApiKeyID,
line: `ri:secret=12345`,
match: `secret=12345`,
want: false,
},
{
name: "no match for xri: prefixed attribute",
ruleID: rules.GenericApiKeyID,
line: `<ri:attachment xri:secret="12345" />`,
match: `secret="12345"`,
want: false,
},
{
name: "no match when rule ID is not generic-api-key does not apply",
ruleID: "some-other-rule",
line: `<ri:attachment ri:secret="12345" />`,
match: `secret="12345"`,
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isSecretFromConfluenceResourceIdentifier(tt.ruleID, tt.line, tt.match)
assert.Equal(t, tt.want, got, "ruleID=%q, line=%q, match=%q", tt.ruleID, tt.line, tt.match)
})
}
}

// if any of these tests fails, we should review isSecretFromConfluenceResourceIdentifier and/or generic-api-key rule
func TestDetectWithConfluenceMetadata(t *testing.T) {
secretsCases := []struct {
Content string
Name string
ShouldFind bool
}{
{
Content: "<ri:user ri:userkey=\"8a7f808362ce64321162ceb20e64321a\" >",
Name: "should not detect from confluence userkey metadata",
ShouldFind: false,
},
}

detector, err := Init(&EngineConfig{})
if err != nil {
t.Fatal(err)
}

for _, secret := range secretsCases {
t.Run(secret.Name, func(t *testing.T) {
secretsChan := make(chan *secrets.Secret, 1)
c := plugins.ConfluencePlugin{}
err = detector.DetectFragment(item{content: &secret.Content}, secretsChan, c.GetName())
if err != nil {
return
}
close(secretsChan)

s := <-secretsChan

if secret.ShouldFind {
assert.Equal(t, s.LineContent, secret.Content)
} else {
assert.Nil(t, s)
}
})
}
}

type item struct {
content *string
id string
Expand Down
4 changes: 3 additions & 1 deletion engine/rules/generic-key.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"github.com/zricethezav/gitleaks/v8/config"
)

const GenericApiKeyID = "generic-api-key"

func GenericCredential() *config.Rule {
regex := generateSemiGenericRegexIncludingXml([]string{
"access",
Expand All @@ -21,7 +23,7 @@ func GenericCredential() *config.Rule {
}, `[\w.=-]{10,150}|[a-z0-9][a-z0-9+/]{11,}={0,3}`, true)

return &config.Rule{
RuleID: "generic-api-key",
RuleID: GenericApiKeyID,
Description: "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.",
Regex: regex,
Keywords: []string{
Expand Down
Loading
Loading