From 6d3eec80ce3e4a96d8a6a633f706f53beb1284b2 Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Sun, 7 Jun 2026 01:56:37 +0300 Subject: [PATCH 1/5] feat: flag social account actions Signed-off-by: kriptoburak --- docs/SECURITY-POLICY.md | 22 +++++++++++ skills/agentguard/action-policies.md | 22 +++++++++++ src/action/detectors/network.ts | 49 +++++++++++++++++++++++++ src/tests/action.test.ts | 55 ++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+) diff --git a/docs/SECURITY-POLICY.md b/docs/SECURITY-POLICY.md index b02ab74..01b7bf0 100644 --- a/docs/SECURITY-POLICY.md +++ b/docs/SECURITY-POLICY.md @@ -180,6 +180,28 @@ Commands matching the safe list are allowed without restriction, **unless** they 6. POST/PUT to untrusted domain → escalate medium → high 7. Domain in allowlist → **ALLOW** (low) +#### Social Account Actions + +Mutating requests to X/Twitter or TweetClaw social account endpoints receive the +`SOCIAL_ACCOUNT_ACTION` risk tag and escalate to **high** risk. These actions can +post tweets, post tweet replies, send direct messages, upload media, create +monitors, register webhooks, or run giveaway draws, so balanced mode prompts the +operator before execution instead of silently allowing the request. + +| Example | Risk | +|---------|------| +| `POST https://api.twitter.com/2/tweets` | high | +| `POST https://xquik.com/api/v1/x/tweets` | high | +| `POST https://xquik.com/api/v1/x/dm/12345` | high | +| `POST https://xquik.com/api/v1/x/media` | high | +| `POST https://xquik.com/api/v1/monitors` | high | +| `POST https://xquik.com/api/v1/webhooks` | high | +| `POST https://xquik.com/api/v1/draws` | high | + +Read-only TweetClaw requests such as tweet search, user lookup, or follower +export remain low risk unless they hit another rule such as secret scanning, +high-risk TLD handling, or webhook exfiltration. + --- ### 4.3 File Operations (`read_file` / `write_file`) diff --git a/skills/agentguard/action-policies.md b/skills/agentguard/action-policies.md index 84776e6..f32fd91 100644 --- a/skills/agentguard/action-policies.md +++ b/skills/agentguard/action-policies.md @@ -51,6 +51,28 @@ Scan request body for sensitive data. Priority determines risk level: 6. POST/PUT to untrusted domain -> escalate medium to high 7. Domain in allowlist -> ALLOW (low) +### Social Account Actions + +Mutating requests to X/Twitter or TweetClaw social account endpoints receive the +`SOCIAL_ACCOUNT_ACTION` risk tag and escalate to high risk. Balanced mode prompts +the operator before execution because these requests can post tweets, post tweet +replies, send direct messages, upload media, create monitors, register webhooks, +or run giveaway draws. + +| Example | Risk | +|---------|------| +| `POST https://api.twitter.com/2/tweets` | high | +| `POST https://xquik.com/api/v1/x/tweets` | high | +| `POST https://xquik.com/api/v1/x/dm/12345` | high | +| `POST https://xquik.com/api/v1/x/media` | high | +| `POST https://xquik.com/api/v1/monitors` | high | +| `POST https://xquik.com/api/v1/webhooks` | high | +| `POST https://xquik.com/api/v1/draws` | high | + +Read-only TweetClaw requests such as tweet search, user lookup, or follower +export remain low risk unless they hit another rule such as secret scanning, +high-risk TLD handling, or webhook exfiltration. + ## Command Execution Detector ### Dangerous Commands (always DENY, critical) diff --git a/src/action/detectors/network.ts b/src/action/detectors/network.ts index 222f747..1bda8d0 100644 --- a/src/action/detectors/network.ts +++ b/src/action/detectors/network.ts @@ -61,6 +61,25 @@ const HIGH_RISK_TLDS = [ '.link', ]; +const SOCIAL_ACCOUNT_DOMAINS = [ + 'api.twitter.com', + 'api.x.com', + 'twitter.com', + 'x.com', + 'xquik.com', +]; + +const SOCIAL_ACCOUNT_PATH_PATTERNS = [ + /^\/api\/v1\/x\/tweets(?:\/|$)/, + /^\/api\/v1\/x\/dm(?:\/|$)/, + /^\/api\/v1\/x\/media(?:\/|$)/, + /^\/api\/v1\/x\/profile(?:\/|$)/, + /^\/api\/v1\/x\/users\/[^/]+\/(?:follow|remove-follower)(?:\/|$)/, + /^\/api\/v1\/monitors(?:\/|$)/, + /^\/api\/v1\/webhooks(?:\/|$)/, + /^\/api\/v1\/draws(?:\/|$)/, +]; + /** * Analyze a network request for security risks */ @@ -201,6 +220,17 @@ export function analyzeNetworkRequest( riskLevel = maxRisk(riskLevel, 'medium'); } + if (mutatingMethod && isSocialAccountAction(domain, request.url)) { + riskTags.push('SOCIAL_ACCOUNT_ACTION'); + evidence.push({ + type: 'social_account_action', + field: 'url', + match: request.url, + description: 'Mutating request can change an X/Twitter or TweetClaw social account state', + }); + riskLevel = maxRisk(riskLevel, 'high'); + } + return { risk_level: riskLevel, risk_tags: riskTags, @@ -230,6 +260,25 @@ function isReadOnlyMethod(method: NetworkMethod): boolean { return method === 'GET' || method === 'HEAD' || method === 'OPTIONS'; } +function isSocialAccountAction(domain: string, url: string): boolean { + const matchesKnownSocialDomain = SOCIAL_ACCOUNT_DOMAINS.some( + (knownDomain) => domain === knownDomain || domain.endsWith('.' + knownDomain) + ); + + if (!matchesKnownSocialDomain) return false; + + try { + const parsed = new URL(url); + const pathname = parsed.pathname.toLowerCase(); + if (domain === 'xquik.com' || domain.endsWith('.xquik.com')) { + return SOCIAL_ACCOUNT_PATH_PATTERNS.some((pattern) => pattern.test(pathname)); + } + return true; + } catch { + return false; + } +} + const RISK_ORDER: Record = { low: 0, medium: 1, diff --git a/src/tests/action.test.ts b/src/tests/action.test.ts index d662a0f..8356b25 100644 --- a/src/tests/action.test.ts +++ b/src/tests/action.test.ts @@ -235,6 +235,61 @@ describe('Network Request Detector', () => { assert.ok(!result.should_block); }); + it('should require high-risk review for TweetClaw social account writes', () => { + const result = analyzeNetworkRequest({ + method: 'POST', + url: 'https://xquik.com/api/v1/x/tweets', + body_preview: '{"text":"Launch update"}', + }); + assert.equal(result.risk_level, 'high'); + assert.ok(result.risk_tags.includes('SOCIAL_ACCOUNT_ACTION')); + assert.ok(result.risk_tags.includes('MUTATING_UNTRUSTED_REQUEST')); + assert.ok(!result.should_block); + }); + + it('should require high-risk review for TweetClaw recurring social workflows', () => { + const result = analyzeNetworkRequest({ + method: 'POST', + url: 'https://xquik.com/api/v1/monitors', + body_preview: '{"username":"example","eventTypes":["tweet"]}', + }); + assert.equal(result.risk_level, 'high'); + assert.ok(result.risk_tags.includes('SOCIAL_ACCOUNT_ACTION')); + assert.ok(!result.should_block); + }); + + it('should require high-risk review for TweetClaw direct messages', () => { + const result = analyzeNetworkRequest({ + method: 'POST', + url: 'https://xquik.com/api/v1/x/dm/12345', + body_preview: '{"text":"hello"}', + }); + assert.equal(result.risk_level, 'high'); + assert.ok(result.risk_tags.includes('SOCIAL_ACCOUNT_ACTION')); + assert.ok(!result.should_block); + }); + + it('should keep read-only TweetClaw searches low risk', () => { + const result = analyzeNetworkRequest({ + method: 'GET', + url: 'https://xquik.com/api/v1/x/tweets/search?query=openclaw', + }); + assert.equal(result.risk_level, 'low'); + assert.ok(!result.risk_tags.includes('SOCIAL_ACCOUNT_ACTION')); + assert.ok(!result.should_block); + }); + + it('should require high-risk review for direct X mutating requests', () => { + const result = analyzeNetworkRequest({ + method: 'POST', + url: 'https://api.twitter.com/2/tweets', + body_preview: '{"text":"Agent-generated reply"}', + }); + assert.equal(result.risk_level, 'high'); + assert.ok(result.risk_tags.includes('SOCIAL_ACCOUNT_ACTION')); + assert.ok(!result.should_block); + }); + it('should normalize lowercase mutating request methods', () => { const postResult = analyzeNetworkRequest({ method: 'post' as NetworkRequestData['method'], From 4296f9d17d70ece6bb11e91fc5e69aaeeacfa91b Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Sun, 7 Jun 2026 02:02:42 +0300 Subject: [PATCH 2/5] fix: narrow social account detection Signed-off-by: kriptoburak --- src/action/detectors/network.ts | 27 +++++++++++++++++---------- src/tests/action.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/action/detectors/network.ts b/src/action/detectors/network.ts index 1bda8d0..faf04f9 100644 --- a/src/action/detectors/network.ts +++ b/src/action/detectors/network.ts @@ -66,10 +66,9 @@ const SOCIAL_ACCOUNT_DOMAINS = [ 'api.x.com', 'twitter.com', 'x.com', - 'xquik.com', ]; -const SOCIAL_ACCOUNT_PATH_PATTERNS = [ +const XQUIK_SOCIAL_ACCOUNT_PATH_PATTERNS = [ /^\/api\/v1\/x\/tweets(?:\/|$)/, /^\/api\/v1\/x\/dm(?:\/|$)/, /^\/api\/v1\/x\/media(?:\/|$)/, @@ -80,6 +79,14 @@ const SOCIAL_ACCOUNT_PATH_PATTERNS = [ /^\/api\/v1\/draws(?:\/|$)/, ]; +const DIRECT_SOCIAL_ACCOUNT_PATH_PATTERNS = [ + /^\/2\/tweets(?:\/|$)/, + /^\/2\/users\/[^/]+\/(?:following|likes|retweets)(?:\/|$)/, + /^\/2\/dm_conversations(?:\/|$)/, + /^\/1\.1\/statuses\/(?:update|destroy)(?:\/|\.json|$)/, + /^\/1\.1\/direct_messages(?:\/|$)/, +]; + /** * Analyze a network request for security risks */ @@ -261,19 +268,19 @@ function isReadOnlyMethod(method: NetworkMethod): boolean { } function isSocialAccountAction(domain: string, url: string): boolean { - const matchesKnownSocialDomain = SOCIAL_ACCOUNT_DOMAINS.some( - (knownDomain) => domain === knownDomain || domain.endsWith('.' + knownDomain) - ); - - if (!matchesKnownSocialDomain) return false; - try { const parsed = new URL(url); const pathname = parsed.pathname.toLowerCase(); if (domain === 'xquik.com' || domain.endsWith('.xquik.com')) { - return SOCIAL_ACCOUNT_PATH_PATTERNS.some((pattern) => pattern.test(pathname)); + return XQUIK_SOCIAL_ACCOUNT_PATH_PATTERNS.some((pattern) => pattern.test(pathname)); } - return true; + const matchesKnownSocialDomain = SOCIAL_ACCOUNT_DOMAINS.some( + (knownDomain) => domain === knownDomain || domain.endsWith('.' + knownDomain) + ); + return ( + matchesKnownSocialDomain && + DIRECT_SOCIAL_ACCOUNT_PATH_PATTERNS.some((pattern) => pattern.test(pathname)) + ); } catch { return false; } diff --git a/src/tests/action.test.ts b/src/tests/action.test.ts index 8356b25..98e3b0e 100644 --- a/src/tests/action.test.ts +++ b/src/tests/action.test.ts @@ -269,6 +269,17 @@ describe('Network Request Detector', () => { assert.ok(!result.should_block); }); + it('should require high-risk review for TweetClaw profile updates', () => { + const result = analyzeNetworkRequest({ + method: 'PATCH', + url: 'https://xquik.com/api/v1/x/profile', + body_preview: '{"bio":"Approved profile update"}', + }); + assert.equal(result.risk_level, 'high'); + assert.ok(result.risk_tags.includes('SOCIAL_ACCOUNT_ACTION')); + assert.ok(!result.should_block); + }); + it('should keep read-only TweetClaw searches low risk', () => { const result = analyzeNetworkRequest({ method: 'GET', @@ -290,6 +301,18 @@ describe('Network Request Detector', () => { assert.ok(!result.should_block); }); + it('should keep direct X non-social mutating requests at generic network risk', () => { + const result = analyzeNetworkRequest({ + method: 'POST', + url: 'https://api.twitter.com/2/oauth2/token', + body_preview: '{"grant_type":"client_credentials"}', + }); + assert.equal(result.risk_level, 'medium'); + assert.ok(result.risk_tags.includes('MUTATING_UNTRUSTED_REQUEST')); + assert.ok(!result.risk_tags.includes('SOCIAL_ACCOUNT_ACTION')); + assert.ok(!result.should_block); + }); + it('should normalize lowercase mutating request methods', () => { const postResult = analyzeNetworkRequest({ method: 'post' as NetworkRequestData['method'], From d058071fdc7294f3b436171a8da2e94fe31707e6 Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Sun, 7 Jun 2026 02:04:01 +0300 Subject: [PATCH 3/5] fix: exclude non-social X API paths Signed-off-by: kriptoburak --- src/action/detectors/network.ts | 8 ++++++++ src/tests/action.test.ts | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/action/detectors/network.ts b/src/action/detectors/network.ts index faf04f9..70b2fb0 100644 --- a/src/action/detectors/network.ts +++ b/src/action/detectors/network.ts @@ -87,6 +87,13 @@ const DIRECT_SOCIAL_ACCOUNT_PATH_PATTERNS = [ /^\/1\.1\/direct_messages(?:\/|$)/, ]; +const DIRECT_SOCIAL_ACCOUNT_EXCLUDED_PATH_PATTERNS = [ + /^\/2\/oauth2(?:\/|$)/, + /^\/oauth2(?:\/|$)/, + /^\/oauth(?:\/|$)/, + /^\/1\.1\/account(?:\/|$)/, +]; + /** * Analyze a network request for security risks */ @@ -279,6 +286,7 @@ function isSocialAccountAction(domain: string, url: string): boolean { ); return ( matchesKnownSocialDomain && + !DIRECT_SOCIAL_ACCOUNT_EXCLUDED_PATH_PATTERNS.some((pattern) => pattern.test(pathname)) && DIRECT_SOCIAL_ACCOUNT_PATH_PATTERNS.some((pattern) => pattern.test(pathname)) ); } catch { diff --git a/src/tests/action.test.ts b/src/tests/action.test.ts index 98e3b0e..fdd4f76 100644 --- a/src/tests/action.test.ts +++ b/src/tests/action.test.ts @@ -313,6 +313,18 @@ describe('Network Request Detector', () => { assert.ok(!result.should_block); }); + it('should keep direct X account-management requests out of social-action review', () => { + const result = analyzeNetworkRequest({ + method: 'POST', + url: 'https://api.x.com/1.1/account/verify_credentials.json', + body_preview: '{}', + }); + assert.equal(result.risk_level, 'medium'); + assert.ok(result.risk_tags.includes('MUTATING_UNTRUSTED_REQUEST')); + assert.ok(!result.risk_tags.includes('SOCIAL_ACCOUNT_ACTION')); + assert.ok(!result.should_block); + }); + it('should normalize lowercase mutating request methods', () => { const postResult = analyzeNetworkRequest({ method: 'post' as NetworkRequestData['method'], From c4d3060c34081b6db9e3dc33a3a384b0a95aa996 Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Sun, 7 Jun 2026 02:07:12 +0300 Subject: [PATCH 4/5] fix: broaden direct X action coverage Signed-off-by: kriptoburak --- src/action/detectors/network.ts | 13 ++----------- src/tests/action.test.ts | 13 ++++++++++++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/action/detectors/network.ts b/src/action/detectors/network.ts index 70b2fb0..9d2f498 100644 --- a/src/action/detectors/network.ts +++ b/src/action/detectors/network.ts @@ -79,19 +79,11 @@ const XQUIK_SOCIAL_ACCOUNT_PATH_PATTERNS = [ /^\/api\/v1\/draws(?:\/|$)/, ]; -const DIRECT_SOCIAL_ACCOUNT_PATH_PATTERNS = [ - /^\/2\/tweets(?:\/|$)/, - /^\/2\/users\/[^/]+\/(?:following|likes|retweets)(?:\/|$)/, - /^\/2\/dm_conversations(?:\/|$)/, - /^\/1\.1\/statuses\/(?:update|destroy)(?:\/|\.json|$)/, - /^\/1\.1\/direct_messages(?:\/|$)/, -]; - const DIRECT_SOCIAL_ACCOUNT_EXCLUDED_PATH_PATTERNS = [ /^\/2\/oauth2(?:\/|$)/, /^\/oauth2(?:\/|$)/, /^\/oauth(?:\/|$)/, - /^\/1\.1\/account(?:\/|$)/, + /^\/1\.1\/account\/verify_credentials(?:\.json|\/|$)/, ]; /** @@ -286,8 +278,7 @@ function isSocialAccountAction(domain: string, url: string): boolean { ); return ( matchesKnownSocialDomain && - !DIRECT_SOCIAL_ACCOUNT_EXCLUDED_PATH_PATTERNS.some((pattern) => pattern.test(pathname)) && - DIRECT_SOCIAL_ACCOUNT_PATH_PATTERNS.some((pattern) => pattern.test(pathname)) + !DIRECT_SOCIAL_ACCOUNT_EXCLUDED_PATH_PATTERNS.some((pattern) => pattern.test(pathname)) ); } catch { return false; diff --git a/src/tests/action.test.ts b/src/tests/action.test.ts index fdd4f76..27f470a 100644 --- a/src/tests/action.test.ts +++ b/src/tests/action.test.ts @@ -313,7 +313,7 @@ describe('Network Request Detector', () => { assert.ok(!result.should_block); }); - it('should keep direct X account-management requests out of social-action review', () => { + it('should keep direct X credential verification out of social-action review', () => { const result = analyzeNetworkRequest({ method: 'POST', url: 'https://api.x.com/1.1/account/verify_credentials.json', @@ -325,6 +325,17 @@ describe('Network Request Detector', () => { assert.ok(!result.should_block); }); + it('should require high-risk review for direct X account updates', () => { + const result = analyzeNetworkRequest({ + method: 'POST', + url: 'https://api.x.com/1.1/account/update_profile.json', + body_preview: '{"description":"Approved update"}', + }); + assert.equal(result.risk_level, 'high'); + assert.ok(result.risk_tags.includes('SOCIAL_ACCOUNT_ACTION')); + assert.ok(!result.should_block); + }); + it('should normalize lowercase mutating request methods', () => { const postResult = analyzeNetworkRequest({ method: 'post' as NetworkRequestData['method'], From 3eed141bdf2ba7c605237b7cb2a57ce238c84c69 Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Sun, 7 Jun 2026 02:08:55 +0300 Subject: [PATCH 5/5] fix: allowlist direct X social actions Signed-off-by: kriptoburak --- src/action/detectors/network.ts | 21 +++++++++++++++++++-- src/tests/action.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/action/detectors/network.ts b/src/action/detectors/network.ts index 9d2f498..aa6512f 100644 --- a/src/action/detectors/network.ts +++ b/src/action/detectors/network.ts @@ -79,6 +79,21 @@ const XQUIK_SOCIAL_ACCOUNT_PATH_PATTERNS = [ /^\/api\/v1\/draws(?:\/|$)/, ]; +const DIRECT_SOCIAL_ACCOUNT_PATH_PATTERNS = [ + /^\/2\/tweets(?:\/|$)/, + /^\/2\/users\/[^/]+\/(?:following|likes|retweets|muting|blocking)(?:\/|$)/, + /^\/2\/dm_conversations(?:\/|$)/, + /^\/2\/dm_events(?:\/|$)/, + /^\/1\.1\/statuses\/(?:update|destroy|retweet|unretweet)(?:\/|\.json|$)/, + /^\/1\.1\/direct_messages(?:\/|$)/, + /^\/1\.1\/account\/(?:update_profile|update_profile_image|update_profile_banner|remove_profile_banner|settings)(?:\.json|\/|$)/, + /^\/1\.1\/friendships\/(?:create|destroy|update)(?:\.json|\/|$)/, + /^\/1\.1\/favorites\/(?:create|destroy)(?:\.json|\/|$)/, + /^\/1\.1\/blocks\/(?:create|destroy)(?:\.json|\/|$)/, + /^\/1\.1\/mutes\/users\/(?:create|destroy)(?:\.json|\/|$)/, + /^\/1\.1\/media\/upload(?:\.json|\/|$)/, +]; + const DIRECT_SOCIAL_ACCOUNT_EXCLUDED_PATH_PATTERNS = [ /^\/2\/oauth2(?:\/|$)/, /^\/oauth2(?:\/|$)/, @@ -101,6 +116,7 @@ export function analyzeNetworkRequest( const method = normalizeMethod(request.method); const readOnlyMethod = isReadOnlyMethod(method); const mutatingMethod = method === 'POST' || method === 'PUT' || method === 'PATCH'; + const stateChangingMethod = mutatingMethod || method === 'DELETE'; // Extract domain const domain = extractDomain(request.url); @@ -226,7 +242,7 @@ export function analyzeNetworkRequest( riskLevel = maxRisk(riskLevel, 'medium'); } - if (mutatingMethod && isSocialAccountAction(domain, request.url)) { + if (stateChangingMethod && isSocialAccountAction(domain, request.url)) { riskTags.push('SOCIAL_ACCOUNT_ACTION'); evidence.push({ type: 'social_account_action', @@ -278,7 +294,8 @@ function isSocialAccountAction(domain: string, url: string): boolean { ); return ( matchesKnownSocialDomain && - !DIRECT_SOCIAL_ACCOUNT_EXCLUDED_PATH_PATTERNS.some((pattern) => pattern.test(pathname)) + !DIRECT_SOCIAL_ACCOUNT_EXCLUDED_PATH_PATTERNS.some((pattern) => pattern.test(pathname)) && + DIRECT_SOCIAL_ACCOUNT_PATH_PATTERNS.some((pattern) => pattern.test(pathname)) ); } catch { return false; diff --git a/src/tests/action.test.ts b/src/tests/action.test.ts index 27f470a..0cd8ce5 100644 --- a/src/tests/action.test.ts +++ b/src/tests/action.test.ts @@ -336,6 +336,29 @@ describe('Network Request Detector', () => { assert.ok(!result.should_block); }); + it('should require high-risk review for direct X tweet deletes', () => { + const result = analyzeNetworkRequest({ + method: 'DELETE', + url: 'https://api.x.com/2/tweets/12345', + body_preview: '{}', + }); + assert.equal(result.risk_level, 'high'); + assert.ok(result.risk_tags.includes('SOCIAL_ACCOUNT_ACTION')); + assert.ok(!result.should_block); + }); + + it('should keep direct X compliance jobs out of social-action review', () => { + const result = analyzeNetworkRequest({ + method: 'POST', + url: 'https://api.x.com/2/compliance/jobs', + body_preview: '{"type":"tweets","name":"audit"}', + }); + assert.equal(result.risk_level, 'medium'); + assert.ok(result.risk_tags.includes('MUTATING_UNTRUSTED_REQUEST')); + assert.ok(!result.risk_tags.includes('SOCIAL_ACCOUNT_ACTION')); + assert.ok(!result.should_block); + }); + it('should normalize lowercase mutating request methods', () => { const postResult = analyzeNetworkRequest({ method: 'post' as NetworkRequestData['method'],