Skip to content

Commit 9014c08

Browse files
committed
fix(connectors): address remaining audit findings
- slack: detect message edits and new threaded replies via edited.ts/latest_reply in contentHash - github: skip binary files via NUL-byte sniff and filter oversized files at tree-list time - servicenow: add 'outdated' workflow state to allowlist and dropdown; add Canceled (8) to incident state dropdown - salesforce: drop 400 from userinfo host fallthrough (Salesforce returns 401/403 for cross-env tokens)
1 parent 2eb96ec commit 9014c08

4 files changed

Lines changed: 44 additions & 5 deletions

File tree

apps/sim/connectors/github/github.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ const GITHUB_API_URL = 'https://api.github.com'
1111
const BATCH_SIZE = 30
1212
const GIT_SHA_PREFIX = 'git-sha:'
1313
const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10 MB
14+
const BINARY_SNIFF_BYTES = 8000
15+
16+
/**
17+
* Heuristic binary detection: Git treats files containing a NUL byte in the
18+
* first 8000 bytes as binary. Matches `git diff` / `git grep` semantics.
19+
*/
20+
function isBinaryBuffer(buf: Buffer): boolean {
21+
const len = Math.min(buf.length, BINARY_SNIFF_BYTES)
22+
for (let i = 0; i < len; i++) {
23+
if (buf[i] === 0) return true
24+
}
25+
return false
26+
}
1427

1528
/**
1629
* Parses the repository string into owner and repo.
@@ -228,10 +241,11 @@ export const githubConnector: ConnectorConfig = {
228241
} else {
229242
const tree = await fetchTree(accessToken, owner, repo, branch)
230243

231-
// Filter by path prefix and extensions
244+
// Filter by path prefix, extensions, and size
232245
const filtered = tree.filter((item) => {
233246
if (pathPrefix && !item.path.startsWith(pathPrefix)) return false
234247
if (!matchesExtension(item.path, extSet)) return false
248+
if (typeof item.size === 'number' && item.size > MAX_FILE_SIZE) return false
235249
return true
236250
})
237251

@@ -311,7 +325,12 @@ export const githubConnector: ConnectorConfig = {
311325
const encoding = data.encoding as string | undefined
312326
let content: string
313327
if (encoding === 'base64' && rawContent.length > 0) {
314-
content = Buffer.from(rawContent, 'base64').toString('utf8')
328+
const buf = Buffer.from(rawContent, 'base64')
329+
if (isBinaryBuffer(buf)) {
330+
logger.info('Skipping binary GitHub file', { path, size })
331+
return null
332+
}
333+
content = buf.toString('utf8')
315334
} else if (encoding === 'none' && data.sha && size > 0) {
316335
content = await fetchBlobContent(accessToken, owner, repo, data.sha as string)
317336
} else {

apps/sim/connectors/salesforce/salesforce.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ async function fetchUserinfo(
102102

103103
// Only fall through to the next host on auth-shaped failures; surface
104104
// other errors (e.g. 5xx) immediately so we don't mask real problems.
105-
if (response.status !== 400 && response.status !== 401 && response.status !== 403) {
105+
if (response.status !== 401 && response.status !== 403) {
106106
break
107107
}
108108
}

apps/sim/connectors/servicenow/servicenow.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const PAGE_SIZE = 100
2323
const SYS_ID_PATTERN = /^[a-f0-9]{32}$/i
2424
const NUMERIC_ID_PATTERN = /^\d+$/
2525
const KB_CATEGORY_PATTERN = /^[\w \-./]+$/
26-
const VALID_WORKFLOW_STATES = new Set(['published', 'draft', 'review', 'retired'])
26+
const VALID_WORKFLOW_STATES = new Set(['published', 'draft', 'review', 'retired', 'outdated'])
2727

2828
interface ServiceNowRecord {
2929
sys_id: string
@@ -474,6 +474,7 @@ export const servicenowConnector: ConnectorConfig = {
474474
{ label: 'Draft', id: 'draft' },
475475
{ label: 'Review', id: 'review' },
476476
{ label: 'Retired', id: 'retired' },
477+
{ label: 'Outdated', id: 'outdated' },
477478
],
478479
},
479480
{
@@ -497,6 +498,7 @@ export const servicenowConnector: ConnectorConfig = {
497498
{ label: 'On Hold', id: '3' },
498499
{ label: 'Resolved', id: '6' },
499500
{ label: 'Closed', id: '7' },
501+
{ label: 'Canceled', id: '8' },
500502
],
501503
},
502504
{

apps/sim/connectors/slack/slack.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ interface SlackMessage {
1717
text?: string
1818
ts: string
1919
subtype?: string
20+
edited?: { ts: string; user?: string }
21+
latest_reply?: string
22+
reply_count?: number
2023
}
2124

2225
interface SlackChannel {
@@ -301,7 +304,22 @@ async function buildSlackChannelDocument(
301304

302305
const content = await formatMessages(accessToken, messages, syncContext)
303306
const messageCount = messages.length
304-
const contentHash = `slack:${channel.id}:${oldestTs ?? 'empty'}:${lastActivityTs ?? 'empty'}:${messageCount}`
307+
308+
/**
309+
* Edit/thread fingerprint: max(edited.ts) and max(latest_reply) across the
310+
* window. `ts` is immutable for messages, so without these signals an
311+
* in-place edit (chat.update) or a new threaded reply would not change the
312+
* channel hash. Slack returns `edited.ts` only when a message was edited
313+
* and `latest_reply` only when threaded replies exist.
314+
*/
315+
let maxEditTs = ''
316+
let maxReplyTs = ''
317+
for (const m of messages) {
318+
if (m.edited?.ts && m.edited.ts > maxEditTs) maxEditTs = m.edited.ts
319+
if (m.latest_reply && m.latest_reply > maxReplyTs) maxReplyTs = m.latest_reply
320+
}
321+
322+
const contentHash = `slack:${channel.id}:${oldestTs ?? 'empty'}:${lastActivityTs ?? 'empty'}:${messageCount}:${maxEditTs || 'noedit'}:${maxReplyTs || 'noreply'}`
305323

306324
return { content, contentHash, messageCount, lastActivityTs }
307325
}

0 commit comments

Comments
 (0)