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
134 changes: 134 additions & 0 deletions .github/workflows/required-secret-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
name: Secret Fields Check

on:
pull_request:
types: [opened, synchronize, reopened]

jobs:
secret-fields-check:
runs-on: ubuntu-22.04
timeout-minutes: 5
strategy:
matrix:
node-version: [22.x]

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: yarn

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Check PR description
id: pr_desc
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const body = (pr?.body || "").toLowerCase();
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converting the PR body to lowercase (line 36) but then testing against case-insensitive regex patterns (with the i flag) is redundant and can cause issues. The pattern on line 42 will never match because it checks for \*\* (literal asterisks for markdown bold) after lowercasing the text, but lowercase conversion doesn't affect asterisks.

Remove the .toLowerCase() call since all patterns already use the i flag for case-insensitive matching:

const body = pr?.body || "";
Suggested change
const body = (pr?.body || "").toLowerCase();
const body = pr?.body || "";

Copilot uses AI. Check for mistakes.

const patterns = [
/-\s*\[x\]\s*required secret checkout/i,
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The pattern /-\s*\[x\]\s*required secret checkout/i on line 39 doesn't match anything in the documented PR template patterns. This appears to be a legacy pattern that should be removed if it's no longer in use, or documented if it's still valid.

Suggested change
/-\s*\[x\]\s*required secret checkout/i,

Copilot uses AI. Check for mistakes.
/required secret checkout\s*:\s*true/i,
// The new checkbox you provided (checked)
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The comment on line 41 says "The new checkbox you provided (checked)" but doesn't clearly explain what this pattern is checking for. Consider updating the comment to be more descriptive, e.g.:

// Pattern to match the security review checkbox from the PR template
Suggested change
// The new checkbox you provided (checked)
// Pattern to match the security review checkbox from the PR template (checked)

Copilot uses AI. Check for mistakes.
/-\s*\[x\]\s*\*\*\s*Reviewed all field definitions\s*\*\*\s*for sensitive data\s*\(API keys, tokens, passwords, client secrets\)\s*and confirmed they use\s*`type:\s*'password'`/i
];

const skip = patterns.some(rx => rx.test(body));
core.setOutput("skip", skip ? "true" : "false");
console.log("Skip secret check:", skip);

- name: Run validate-secret-fields
id: list
if: steps.pr_desc.outputs.skip != 'true'
uses: actions/github-script@v7
with:
script: |
const { execSync } = require("child_process");

let raw = execSync("./bin/run validate-secret-fields", {
encoding: "utf8"
});

const parsed = JSON.parse(raw);

const findings = [];
for (const [destination, obj] of Object.entries(parsed)) {
if (Array.isArray(obj.settings) && obj.settings.length > 0) {
findings.push({
destination,
settings: obj.settings
});
}
Comment on lines +64 to +70
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow only checks for obj.settings but ignores action-level secret fields. Since the CLI command's description states it returns "secret fields for each action", the workflow should also iterate through action keys in the parsed output to detect secret fields in individual actions, not just destination settings.

This is related to the CLI bug where actions aren't being checked, but even if that's fixed, this workflow logic needs to be updated to handle action-level findings.

Suggested change
for (const [destination, obj] of Object.entries(parsed)) {
if (Array.isArray(obj.settings) && obj.settings.length > 0) {
findings.push({
destination,
settings: obj.settings
});
}
for (const [destination, obj] of Object.entries(parsed)) {
// Check destination-level settings
if (Array.isArray(obj.settings) && obj.settings.length > 0) {
findings.push({
destination,
settings: obj.settings
});
}
// Check action-level secret fields
if (obj.actions && typeof obj.actions === "object") {
for (const [actionName, actionObj] of Object.entries(obj.actions)) {
if (Array.isArray(actionObj) && actionObj.length > 0) {
findings.push({
destination,
action: actionName,
fields: actionObj
});
}
}
}

Copilot uses AI. Check for mistakes.
}

core.setOutput("findings", JSON.stringify(findings));
console.log("Findings:", findings);

- name: Comment if secret fields found
if: steps.pr_desc.outputs.skip != 'true'
uses: actions/github-script@v7
with:
findings: ${{ steps.list.outputs.findings }}
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The findings input is declared but never used in the script. The script retrieves findings via core.getInput("findings") on line 82, but GitHub Actions doesn't automatically pass declared inputs to the script context unless they're referenced in the with section as ${{ steps.list.outputs.findings }}.

The current approach works because step outputs are available via core.getInput(), but the findings: declaration on line 80 is misleading and should be removed to avoid confusion.

Suggested change
findings: ${{ steps.list.outputs.findings }}

Copilot uses AI. Check for mistakes.
script: |
const findings = JSON.parse(core.getInput("findings") || "[]");

// If NO secrets found → exit quietly and succeed
if (findings.length === 0) {
console.log("No secret fields found — skipping comment.");
return;
}

// Build comment markdown
const lines = findings
.map(f => `- **Destination**: ${f.destination}\n - Settings: ${f.settings.join(", ")}`)
.join("\n");

const body = `<!--SECRET_FIELDS_FOUND-->
### ⚠️ Secret Fields Detected

The following destinations require secret \`settings\` values that require review:

${lines}

This PR cannot be merged until these are removed or converted to secure secrets.
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message "This PR cannot be merged until these are removed or converted to secure secrets" is unclear and potentially misleading. The fields themselves aren't removed - they need to be marked with type: 'password' to properly protect sensitive data.

Consider a clearer message such as:

⚠️ **Action Required**: The following fields appear to contain sensitive data based on their names but are not using `type: 'password'`. Please review these fields and update them to use `type: 'password'` if they contain API keys, tokens, passwords, or other secrets.
Suggested change
This PR cannot be merged until these are removed or converted to secure secrets.
⚠️ **Action Required**: The following fields appear to contain sensitive data based on their names but are not using \`type: 'password'\`. Please review these fields and update them to use \`type: 'password'\` if they contain API keys, tokens, passwords, or other secrets.

Copilot uses AI. Check for mistakes.
`;

// Find existing comment
const comments = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo
});

const existing = comments.data.find(c =>
c.body?.includes("<!--SECRET_FIELDS_FOUND-->")
);

if (!existing) {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
});
} else {
await github.rest.issues.updateComment({
comment_id: existing.id,
owner: context.repo.owner,
repo: context.repo.repo,
body
});
}

core.setFailed(
`Secret-like settings found in ${findings.length} destination(s).`
);
98 changes: 98 additions & 0 deletions packages/cli/src/commands/validate-secret-fields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Command, flags } from '@oclif/command'
import { loadDestination } from '../lib/destinations'
import globby from 'globby'
import type { DestinationDefinition as CloudDestinationDefinition } from '@segment/actions-core'
import type { BrowserDestinationDefinition } from '@segment/destinations-manifest'

export default class ValidateSecretFields extends Command {
public static enableJsonFlag = true

static description = `Returns a list of secret fields for each action in the destination.`
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description states the command "Returns a list of secret fields for each action in the destination", but the current implementation only checks destination-level settings and authentication fields, not individual actions. The description should be updated to accurately reflect what the command does, or the implementation should be fixed to match the description.

Copilot uses AI. Check for mistakes.

static examples = [
`$ ./bin/run validate-secret-fields`,
`$ ./bin/run validate-secret-fields -p ./packages/destination-actions/src/destinations/hubspot/index.ts`
]

// eslint-disable-next-line @typescript-eslint/no-explicit-any
static flags: flags.Input<any> = {
help: flags.help({ char: 'h' }),
path: flags.string({
char: 'p',
description: 'file path for the integration(s). Accepts glob patterns.',
multiple: true
})
}

static args = []

async run() {
const { flags } = this.parse(ValidateSecretFields)

const globs = flags.path || [
'./packages/*/src/destinations/*/index.ts',
'./packages/browser-destinations/destinations/*/src/index.ts'
]

const files = await globby(globs, {
expandDirectories: false,
gitignore: true,
ignore: ['node_modules']
})

const destinationMap: Record<string, Record<string, string[]>> = {}
const actionToSecretFields: Record<string, string[]> = {}
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable actionToSecretFields is declared but never populated with any values. According to line 10, the command should "Returns a list of secret fields for each action in the destination", but the code only checks settings/authentication fields and never iterates through destination.actions to check action fields. This means action-level fields with secret-like names will not be detected.

To fix this, add logic similar to list-required-fields.ts (lines 89-98) to iterate through actions and check their fields for secret patterns.

Copilot uses AI. Check for mistakes.

const cloudDestinationRegex = new RegExp(/packages\/destination-actions\/src\/destinations\/([^/]+)\/*/i)
const browserDestinationRegex = new RegExp(/packages\/browser-destinations\/destinations\/([^/]+)\/*/i)

for (const file of files) {
let filePath = file
// extract the destination folder name from the file path
const cloudMatch = cloudDestinationRegex.exec(file)
const browserMatch = browserDestinationRegex.exec(file)
if (cloudMatch) {
const destination = cloudMatch[1]
if (destinationMap[destination]) {
continue
}
filePath = `packages/destination-actions/src/destinations/${cloudMatch[1]}/index.ts`
} else if (browserMatch) {
const destination = browserMatch[1]
if (destinationMap[destination]) {
continue
}
filePath = `packages/browser-destinations/destinations/${browserMatch[1]}/src/index.ts`
}

const destination = await loadDestination(filePath).catch((error) => {
this.debug(`Couldn't load ${filePath}: ${error.message}`)
return null
})

if (!destination) {
continue
}

const settings = {
...(destination as BrowserDestinationDefinition).settings,
...(destination as CloudDestinationDefinition).authentication?.fields
}

destinationMap[destination.name] = { settings: [] }
const secretFieldPattern = /(^|[^A-Za-z0-9])(key|token|secret|password|code)($|[^A-Za-z0-9])/i
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern /(^|[^A-Za-z0-9])(key|token|secret|password|code)($|[^A-Za-z0-9])/i will incorrectly flag common non-secret fields that contain "code" as a substring, such as:

  • postalCode (very common for address data)
  • countryCode
  • statusCode
  • currencyCode

The pattern should be more specific or exclude known false positives. Consider using word boundaries or a more selective pattern like /(api.?key|access.?token|auth.?token|secret|password|client.?secret)/i that targets actual secret field naming patterns.

Suggested change
const secretFieldPattern = /(^|[^A-Za-z0-9])(key|token|secret|password|code)($|[^A-Za-z0-9])/i
const secretFieldPattern = /^(api.?key|access.?token|auth.?token|secret|password|client.?secret)$/i

Copilot uses AI. Check for mistakes.
for (const [key, field] of Object.entries(settings)) {
if (secretFieldPattern.test(key) && field.type !== 'password') {
destinationMap[destination.name].settings.push(key)
}
}

destinationMap[destination.name] = {
...destinationMap[destination.name],
...actionToSecretFields
}
Comment on lines +90 to +93
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line overwrites the destinationMap[destination.name] object that was just populated with settings on line 82-88, replacing it with a shallow merge that includes the empty actionToSecretFields object. This results in the correct structure but is confusing and inefficient.

Instead, initialize the object once with the correct structure:

destinationMap[destination.name] = { 
  settings: [],
  ...actionToSecretFields 
}

Then populate the settings array in the loop.

Copilot uses AI. Check for mistakes.
}

this.log(JSON.stringify(destinationMap, null, 2))
}
}
Loading