mirror of
https://github.com/larksuite/cli.git
synced 2026-07-10 02:54:04 +08:00
Compare commits
1 Commits
feat/plugi
...
fix/public
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c4a1e0188 |
@@ -134,6 +134,9 @@ func isCredentialAssignmentMatch(match string) bool {
|
||||
if isBenignTokenField(name) && !credentialShapedValue(value) {
|
||||
return false
|
||||
}
|
||||
if isCredentialIdentifierField(name) && !credentialShapedValue(value) {
|
||||
return false
|
||||
}
|
||||
if isWeakTokenCredentialKey(name) && !weakTokenValueLooksCredentialLike(value) {
|
||||
return false
|
||||
}
|
||||
@@ -225,6 +228,17 @@ func isTokenMetadataField(key string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func isCredentialIdentifierField(key string) bool {
|
||||
switch {
|
||||
case key == "api_key_id":
|
||||
return true
|
||||
case strings.HasSuffix(key, "_api_key_id"):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isPaginationOrSyncTokenField(key string) bool {
|
||||
switch key {
|
||||
case "page_token",
|
||||
@@ -493,12 +507,18 @@ func isBenignCodeCredentialExpression(file, line, match, value string) bool {
|
||||
if strings.HasPrefix(normalized, "regexp.MustCompile(") {
|
||||
return true
|
||||
}
|
||||
if !sourceCodeFile(file) || credentialShapedValue(value) {
|
||||
return false
|
||||
if !sourceCodeFile(file) {
|
||||
return testFixtureFile(file) && testFixtureCredentialLiteral(normalized)
|
||||
}
|
||||
if rhs, ok := sourceCodeTypedCredentialRHS(line, match); ok {
|
||||
return isBenignTypedCredentialRHS(rhs)
|
||||
}
|
||||
if testFixtureFile(file) && testFixtureCredentialLiteral(normalized) {
|
||||
return true
|
||||
}
|
||||
if credentialShapedValue(value) {
|
||||
return false
|
||||
}
|
||||
rawValueQuoted := credentialAssignmentRawValueQuoted(match)
|
||||
if sourceCodeLiteralLooksNonSecret(normalized, !rawValueQuoted) {
|
||||
return true
|
||||
@@ -568,7 +588,7 @@ func credentialAssignmentRawValueQuoted(match string) bool {
|
||||
|
||||
func sourceCodeFile(file string) bool {
|
||||
switch filepath.Ext(file) {
|
||||
case ".go", ".js", ".jsx", ".py", ".ts", ".tsx":
|
||||
case ".go", ".js", ".jsx", ".py", ".sh", ".ts", ".tsx":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -593,6 +613,9 @@ func sourceCodeLiteralLooksNonSecret(value string, allowNumeric bool) bool {
|
||||
sourceCodeFakeOrPlaceholderLiteral(literal) ||
|
||||
sourceCodeCredentialTermLiteral(literal) ||
|
||||
sourceCodeCredentialPrefixLiteral(literal) ||
|
||||
sourceCodeLowEvidenceFixtureLiteral(literal) ||
|
||||
sourceCodeStringExpressionLiteral(literal) ||
|
||||
sourceCodeSyntheticIdentifierLiteral(literal) ||
|
||||
sourceCodeVocabularyLiteral(literal) ||
|
||||
sourceCodeSchemaTypeLiteral(literal) ||
|
||||
benignCredentialStatusLiteral(literal)
|
||||
@@ -685,6 +708,143 @@ func sourceCodeCredentialPrefixLiteral(value string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func sourceCodeLowEvidenceFixtureLiteral(value string) bool {
|
||||
normalized := normalizeFixtureCredentialLiteral(value)
|
||||
return simpleFixtureCredentialLiteral(normalized)
|
||||
}
|
||||
|
||||
func sourceCodeStringExpressionLiteral(value string) bool {
|
||||
normalized := strings.TrimSpace(value)
|
||||
if normalized == "" ||
|
||||
credentialShapedIdentifier(strings.ToLower(normalized)) ||
|
||||
highEntropyCredentialValue(strings.ToLower(normalized)) {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(normalized, "${") ||
|
||||
strings.Contains(normalized, "$(") ||
|
||||
(strings.Contains(normalized, `\b`) && strings.ContainsAny(normalized, "|[]{}()+*?"))
|
||||
}
|
||||
|
||||
func sourceCodeSyntheticIdentifierLiteral(value string) bool {
|
||||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||
if normalized == "" ||
|
||||
strings.HasPrefix(normalized, "real_") ||
|
||||
strings.HasPrefix(normalized, "real-") ||
|
||||
credentialShapedIdentifier(normalized) ||
|
||||
highEntropyCredentialValue(normalized) ||
|
||||
!delimitedPlaceholderIdentifier(normalized) ||
|
||||
!strings.ContainsAny(normalized, "_-") {
|
||||
return false
|
||||
}
|
||||
var parts int
|
||||
for _, part := range strings.FieldsFunc(normalized, func(r rune) bool {
|
||||
return r == '_' || r == '-'
|
||||
}) {
|
||||
if part != "" {
|
||||
parts++
|
||||
}
|
||||
}
|
||||
return parts >= 2
|
||||
}
|
||||
|
||||
func testFixtureCredentialLiteral(value string) bool {
|
||||
normalized := normalizeFixtureCredentialLiteral(value)
|
||||
if simpleFixtureCredentialLiteral(normalized) {
|
||||
return true
|
||||
}
|
||||
switch normalized {
|
||||
case "real-token",
|
||||
"real-tenant-access-token":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeFixtureCredentialLiteral(value string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(strings.Trim(value, `"'`)))
|
||||
if before, _, ok := strings.Cut(normalized, `\n`); ok {
|
||||
normalized = before
|
||||
}
|
||||
if before, _, ok := strings.Cut(normalized, `\r`); ok {
|
||||
normalized = before
|
||||
}
|
||||
if before, _, ok := strings.Cut(normalized, "\n"); ok {
|
||||
normalized = before
|
||||
}
|
||||
if before, _, ok := strings.Cut(normalized, "\r"); ok {
|
||||
normalized = before
|
||||
}
|
||||
for {
|
||||
next := strings.TrimSuffix(normalized, `\n`)
|
||||
next = strings.TrimSuffix(next, `\r`)
|
||||
next = strings.TrimSuffix(next, "\n")
|
||||
next = strings.TrimSuffix(next, "\r")
|
||||
if next == normalized {
|
||||
break
|
||||
}
|
||||
normalized = strings.TrimSpace(next)
|
||||
}
|
||||
normalized = strings.TrimSpace(normalized)
|
||||
normalized = strings.TrimPrefix(normalized, `\"`)
|
||||
normalized = strings.TrimSuffix(normalized, `\"`)
|
||||
return strings.TrimSpace(strings.Trim(normalized, `"'`))
|
||||
}
|
||||
|
||||
func simpleFixtureCredentialLiteral(value string) bool {
|
||||
if value == "" || credentialShapedIdentifier(value) || highEntropyCredentialValue(value) {
|
||||
return false
|
||||
}
|
||||
if len(value) <= 3 && delimitedPlaceholderIdentifier(value) {
|
||||
return true
|
||||
}
|
||||
switch value {
|
||||
case "pass",
|
||||
"pat-token",
|
||||
"password",
|
||||
"pat_abc",
|
||||
"pw",
|
||||
"s3cret",
|
||||
"secret",
|
||||
"secret_only",
|
||||
"test":
|
||||
return true
|
||||
default:
|
||||
return allXPlaceholder(value) || humanReadableFixtureCredentialLiteral(value)
|
||||
}
|
||||
}
|
||||
|
||||
func humanReadableFixtureCredentialLiteral(value string) bool {
|
||||
if !delimitedPlaceholderIdentifier(value) {
|
||||
return false
|
||||
}
|
||||
normalized := strings.ReplaceAll(value, "-", "_")
|
||||
for _, marker := range []string{
|
||||
"auto",
|
||||
"dummy",
|
||||
"example",
|
||||
"fake",
|
||||
"fixture",
|
||||
"hermes",
|
||||
"new",
|
||||
"replace",
|
||||
"restored",
|
||||
"sample",
|
||||
"super",
|
||||
"test",
|
||||
"valid",
|
||||
} {
|
||||
if normalized == marker ||
|
||||
strings.HasPrefix(normalized, marker) ||
|
||||
strings.HasSuffix(normalized, marker) ||
|
||||
strings.Contains(normalized, marker+"_") ||
|
||||
strings.Contains(normalized, "_"+marker) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func sourceCodeVocabularyLiteral(value string) bool {
|
||||
switch strings.ToLower(value) {
|
||||
case "bot", "tenant", "user":
|
||||
@@ -753,7 +913,7 @@ func codeIdentifier(value string) bool {
|
||||
|
||||
func isNonSecretLiteralValue(value string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(strings.Trim(value, `"'`))) {
|
||||
case "true", "false", "null", "nil", "{", "[":
|
||||
case "true", "false", "null", "nil", "{", "[", `\`:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -980,6 +1140,7 @@ func credentialURLPasswordFixture(password string) bool {
|
||||
normalized := strings.ToLower(strings.Trim(password, `"'`))
|
||||
switch normalized {
|
||||
case "p",
|
||||
"p%40ss",
|
||||
"pass",
|
||||
"password",
|
||||
"pat_abc",
|
||||
@@ -1002,6 +1163,20 @@ func sourceOrTestFixtureFile(file string) bool {
|
||||
strings.Contains(normalized, "/fixtures/")
|
||||
}
|
||||
|
||||
func testFixtureFile(file string) bool {
|
||||
normalized := filepath.ToSlash(file)
|
||||
base := filepath.Base(normalized)
|
||||
return strings.Contains(base, "_test.") ||
|
||||
strings.Contains(base, ".test.") ||
|
||||
strings.Contains(base, "sample") ||
|
||||
strings.HasPrefix(normalized, "tests/") ||
|
||||
strings.HasPrefix(normalized, "testdata/") ||
|
||||
strings.HasPrefix(normalized, "fixtures/") ||
|
||||
strings.Contains(normalized, "/tests/") ||
|
||||
strings.Contains(normalized, "/testdata/") ||
|
||||
strings.Contains(normalized, "/fixtures/")
|
||||
}
|
||||
|
||||
func warnForPrivateIPv4(file string) bool {
|
||||
normalized := filepath.ToSlash(file)
|
||||
if sourceOrTestFixtureFile(normalized) {
|
||||
|
||||
@@ -648,6 +648,7 @@ func TestScanFileAllowsCredentialURLPlaceholders(t *testing.T) {
|
||||
func TestScanFileAllowsCredentialURLFixtures(t *testing.T) {
|
||||
got := ScanFile("fixtures/network_test.go", []byte(strings.Join([]string{
|
||||
`proxy := "http://user:pass@proxy:8080"`,
|
||||
`proxy := "http://user:p%40ss@proxy:8080/path"`,
|
||||
`repo := "https://u:t@h/r.git"`,
|
||||
`target := "https://attacker:pw@open.feishu.cn"`,
|
||||
`proxy := "http://admin:s3cret@127.0.0.1:3128"`,
|
||||
@@ -840,7 +841,24 @@ func TestScanFileDetectsStrongAuthTokenKeysWithFixtureLikeValues(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestScanFileAllowsTestFixtureSecretValues(t *testing.T) {
|
||||
got := ScanFile("fixtures/calendar_meeting_test.go", []byte(`AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,`+"\n"))
|
||||
got := ScanFile("fixtures/calendar_meeting_test.go", []byte(strings.Join([]string{
|
||||
`AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,`,
|
||||
`cfg := &core.CliConfig{AppID: "a", AppSecret: "s"}`,
|
||||
`os.WriteFile(path, []byte("FEISHU_APP_ID=cli_abc\nFEISHU_APP_SECRET=secret\n"), 0600)`,
|
||||
`rt := &stubRoundTripper{respBody: ` + "`" + `{"access_token":"t","token_type":"Bearer"}` + "`" + `}`,
|
||||
`cred := credential.NewCredentialProvider([]extcred.Provider{&fakeExtProvider{token: "real-token"}}, nil, nil, nil)`,
|
||||
`const realToken = "real-tenant-access-token"`,
|
||||
`envContent := "FEISHU_APP_ID=cli_hermes_abc\nFEISHU_APP_SECRET=hermes_secret_123\nFEISHU_DOMAIN=lark\n"`,
|
||||
`content := "# Hermes config\nFEISHU_APP_ID=cli_abc123\nFEISHU_APP_SECRET=supersecret\n"`,
|
||||
`os.WriteFile(path, []byte("FEISHU_APP_ID=cli_auto\nFEISHU_APP_SECRET=auto_secret\n"), 0600)`,
|
||||
`os.WriteFile(path, []byte("FEISHU_APP_ID=cli_new_app\nFEISHU_APP_SECRET=new_secret\n"), 0600)`,
|
||||
`if got := out.String(); got != "username=x-access-token\npassword=valid-pat\n\n" {`,
|
||||
`if got := out.String(); got != "username=x-access-token\npassword=restored-pat\n\n" {`,
|
||||
`if got := stdout.String(); got != "username=x-access-token\npassword=pat-token\n\n" {`,
|
||||
`return &core.CliConfig{AppID: "dummy", AppSecret: "dummy"}`,
|
||||
`os.WriteFile(path, []byte("API_KEY=replace-me\n"), 0600)`,
|
||||
`body := "APP_ID=\"cli_xxxxx\"\nAPP_SECRET=\"xxxxx\"\n"`,
|
||||
}, "\n")+"\n"))
|
||||
for _, item := range got {
|
||||
if item.Rule == "public_content_generic_credential" {
|
||||
t.Fatalf("test fixture secret should not be credential finding: %#v", got)
|
||||
@@ -848,8 +866,41 @@ func TestScanFileAllowsTestFixtureSecretValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanFileAllowsCredentialIdentifierFields(t *testing.T) {
|
||||
got := ScanFile("fixtures/openapi_key_test.go", []byte(strings.Join([]string{
|
||||
`"api_key_id": "k1",`,
|
||||
`"secret_id": "s1",`,
|
||||
`"token_id": "t1",`,
|
||||
`"private_key_id": "pk1",`,
|
||||
}, "\n")+"\n"))
|
||||
for _, item := range got {
|
||||
if item.Rule == "public_content_generic_credential" {
|
||||
t.Fatalf("credential identifier fields should not be credential findings: %#v", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanFileDetectsCredentialShapedIdentifierFieldValues(t *testing.T) {
|
||||
got := ScanFile("fixtures/openapi_key_test.go", []byte(strings.Join([]string{
|
||||
`"api_key_id": "real-api-key-id",`,
|
||||
`"token_id": "ghp_1234567890abcdef1234567890abcdef1234",`,
|
||||
}, "\n")+"\n"))
|
||||
var count int
|
||||
for _, item := range got {
|
||||
if item.Rule == "public_content_generic_credential" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count != 2 {
|
||||
t.Fatalf("credential-shaped identifier field findings = %d, want 2: %#v", count, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanFileAllowsRegexpTokenValidators(t *testing.T) {
|
||||
got := ScanFile("fixtures/minutes_detail.go", []byte("var validMinuteTokenDetail = regexp.MustCompile(`^[a-z0-9]+$`)\n"))
|
||||
got := ScanFile("fixtures/minutes_detail.go", []byte(strings.Join([]string{
|
||||
"var validMinuteTokenDetail = regexp.MustCompile(`^[a-z0-9]+$`)",
|
||||
"REALISTIC_TOKEN_RE=\"\\\"${TOKEN_BODY}\\\"|\\`${TOKEN_BODY}\\`|\\\\b${TOKEN_BODY}\\\\b\"",
|
||||
}, "\n")+"\n"))
|
||||
for _, item := range got {
|
||||
if item.Rule == "public_content_generic_credential" {
|
||||
t.Fatalf("regexp token validator should not be credential finding: %#v", got)
|
||||
@@ -927,6 +978,22 @@ func TestScanFileAllowsSourceCodeCredentialNonSecretLiterals(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanFileAllowsSourceCodeSyntheticCredentialIdentifiers(t *testing.T) {
|
||||
got := ScanFile("fixtures/sheets_media.go", []byte(strings.Join([]string{
|
||||
`const fakeOfficeTokenPrefix = "fake_office_"`,
|
||||
`const localOfficeTokenPrefix = "local_office_"`,
|
||||
`const imageLiveSecretMarker = "img_live_secret"`,
|
||||
`const imageProdKeyMarker = "img_prod_key"`,
|
||||
`if strings.HasPrefix(spreadsheetToken, fakeOfficeTokenPrefix) {`,
|
||||
`if strings.HasPrefix(spreadsheetToken, localOfficeTokenPrefix) {`,
|
||||
}, "\n")+"\n"))
|
||||
for _, item := range got {
|
||||
if item.Rule == "public_content_generic_credential" {
|
||||
t.Fatalf("source code token prefix references should not be credential findings: %#v", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanFileAllowsCredentialLikePublicPlaceholders(t *testing.T) {
|
||||
got := ScanFile("fixtures/placeholders.md", []byte(strings.Join([]string{
|
||||
`app_secret=***`,
|
||||
|
||||
Reference in New Issue
Block a user