Skip to content
Merged
Show file tree
Hide file tree
Changes from 33 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
4 changes: 2 additions & 2 deletions .ci/update-readme.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ update_readme() {
}

# Update the README with the help message
help_message=$(go run .)
help_message=$(GOEXPERIMENT=jsonv2 go run .)

echo "" >output.txt
echo '```text' >>output.txt
Expand All @@ -23,7 +23,7 @@ echo "" >>output.txt
update_readme "output.txt" "command-line" "README.md"
rm output.txt

go run . rules | awk 'BEGIN{FS = " *"}{print "| " $1 " | " $2 " | " $3 " | " $4 " |";}' >output.txt
GOEXPERIMENT=jsonv2 go run . rules | awk 'BEGIN{FS = " *"}{print "| " $1 " | " $2 " | " $3 " | " $4 " |";}' >output.txt
update_readme "output.txt" "table" "./docs/list-of-rules.md"
rm output.txt

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codecov.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:

- name: Run tests and generate coverage
run: |
go test ./... -coverpkg=./... -v -coverprofile cover.out
GOEXPERIMENT=jsonv2 go test ./... -coverpkg=./... -v -coverprofile cover.out


- name: Upload coverage to Codecov
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ jobs:
git diff --exit-code

- name: Go Linter
run: docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:v2.1.5 golangci-lint run --timeout=5m
run: docker run --rm -e GOEXPERIMENT=jsonv2 -v $(pwd):/app -w /app golangci/golangci-lint:v2.5.0 golangci-lint run --timeout=5m

- name: Go Test
run: go test -v ./...
run: GOEXPERIMENT=jsonv2 go test -v ./...

- name: Run 2ms Scan
run: go run . git . --config .2ms.yml
run: GOEXPERIMENT=jsonv2 go run . git . --config .2ms.yml

build:
runs-on: ubuntu-latest
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ jobs:

- uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
with:
go-version: "^1.22"
go-version-file: 'go.mod'
- name: Go Linter
run: docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:v2.1.5 golangci-lint run --timeout=5m
run: docker run --rm -e GOEXPERIMENT=jsonv2 -v $(pwd):/app -w /app golangci/golangci-lint:v2.5.0 golangci-lint run --timeout=5m

- name: Unit Tests
run: go test ./...
run: GOEXPERIMENT=jsonv2 go test ./...

- name: Gets release info
id: semantic_release_info
Expand Down
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
# 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.2-r0-1362f4e5a16bb5@sha256:1362f4e5a16bb5dd639020ca7890c99245ab4111f8b3bf360eac87df79e2f4cf AS builder

ARG GOEXPERIMENT=jsonv2
ENV GOEXPERIMENT=$GOEXPERIMENT

WORKDIR /app

Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ RESET := $(shell printf "\033[0m")

COVERAGE_REQUIRED := 55
MOCKGEN_VERSION := 0.5.2
LINTER_VERSION := 2.1.6
LINTER_VERSION := 2.5.0

.PHONY: lint
lint: check-linter-version
Expand All @@ -26,7 +26,7 @@ modtidy:

.PHONY: test
test:
go test -race -vet all -coverprofile=cover.out.tmp ./...
GOEXPERIMENT=jsonv2 go test -race -vet all -coverprofile=cover.out.tmp ./...
grep -v -e "_mock\.go:" -e "/mocks/" -e "/docs/" cover.out.tmp > cover.out
go tool cover -func=cover.out
rm cover.out.tmp
Expand Down
52 changes: 36 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,50 @@ 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 for Basic Auth. |
| `--token` | string | | Confluence **API token** for Basic Auth. |

For example:
#### Authentication
- To scan **private spaces**, provide `--username` and `--token` (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
```

- Scan **private pages** (requires auth):
```bash
2ms confluence https://<company id>.atlassian.net/wiki --username <USERNAME> --token <API_TOKEN>
```

- To scan public spaces:
- Scan specific **spaces by key**:
```bash
2ms confluence https://<company id>.atlassian.net/wiki --space-keys Key1,Key2
```

- Scan specific **spaces by ID**:
```bash
2ms confluence https://checkmarx.atlassian.net/wiki --spaces secrets
2ms confluence https://<company id>.atlassian.net/wiki --space-ids 1234567890,9876543210
```
💡 [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 **pages by ID**:
```bash
2ms confluence <URL> --username <USERNAME> --token <API_TOKEN> --spaces <SPACES>
2ms confluence https://<company id>.atlassian.net/wiki --page-ids 11223344556,99887766554
```
[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)
- 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 @@ -717,6 +717,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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/checkmarx/2ms/v4

go 1.24.6
go 1.25.2

replace (
golang.org/x/oauth2 => golang.org/x/oauth2 v0.30.0
Expand Down
Loading
Loading