@arcadialdev/arcality 2.4.29 → 2.4.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/package.json +3 -4
  2. package/src/arcalityClient.mjs +2 -1
  3. package/.agents/skills/e2e-testing-expert/SKILL.md +0 -28
  4. package/.agents/skills/frontend-design/LICENSE.txt +0 -177
  5. package/.agents/skills/frontend-design/SKILL.md +0 -42
  6. package/.agents/skills/nodejs-backend-patterns/SKILL.md +0 -639
  7. package/.agents/skills/nodejs-backend-patterns/references/advanced-patterns.md +0 -430
  8. package/.agents/skills/playwright-best-practices/LICENSE.md +0 -7
  9. package/.agents/skills/playwright-best-practices/README.md +0 -147
  10. package/.agents/skills/playwright-best-practices/SKILL.md +0 -303
  11. package/.agents/skills/playwright-best-practices/advanced/authentication-flows.md +0 -360
  12. package/.agents/skills/playwright-best-practices/advanced/authentication.md +0 -871
  13. package/.agents/skills/playwright-best-practices/advanced/clock-mocking.md +0 -364
  14. package/.agents/skills/playwright-best-practices/advanced/mobile-testing.md +0 -409
  15. package/.agents/skills/playwright-best-practices/advanced/multi-context.md +0 -288
  16. package/.agents/skills/playwright-best-practices/advanced/multi-user.md +0 -393
  17. package/.agents/skills/playwright-best-practices/advanced/network-advanced.md +0 -452
  18. package/.agents/skills/playwright-best-practices/advanced/third-party.md +0 -464
  19. package/.agents/skills/playwright-best-practices/architecture/pom-vs-fixtures.md +0 -363
  20. package/.agents/skills/playwright-best-practices/architecture/test-architecture.md +0 -369
  21. package/.agents/skills/playwright-best-practices/architecture/when-to-mock.md +0 -383
  22. package/.agents/skills/playwright-best-practices/browser-apis/browser-apis.md +0 -391
  23. package/.agents/skills/playwright-best-practices/browser-apis/iframes.md +0 -403
  24. package/.agents/skills/playwright-best-practices/browser-apis/service-workers.md +0 -504
  25. package/.agents/skills/playwright-best-practices/browser-apis/websockets.md +0 -403
  26. package/.agents/skills/playwright-best-practices/core/annotations.md +0 -424
  27. package/.agents/skills/playwright-best-practices/core/assertions-waiting.md +0 -361
  28. package/.agents/skills/playwright-best-practices/core/configuration.md +0 -452
  29. package/.agents/skills/playwright-best-practices/core/fixtures-hooks.md +0 -417
  30. package/.agents/skills/playwright-best-practices/core/global-setup.md +0 -434
  31. package/.agents/skills/playwright-best-practices/core/locators.md +0 -242
  32. package/.agents/skills/playwright-best-practices/core/page-object-model.md +0 -315
  33. package/.agents/skills/playwright-best-practices/core/projects-dependencies.md +0 -453
  34. package/.agents/skills/playwright-best-practices/core/test-data.md +0 -492
  35. package/.agents/skills/playwright-best-practices/core/test-suite-structure.md +0 -361
  36. package/.agents/skills/playwright-best-practices/core/test-tags.md +0 -298
  37. package/.agents/skills/playwright-best-practices/debugging/console-errors.md +0 -420
  38. package/.agents/skills/playwright-best-practices/debugging/debugging.md +0 -504
  39. package/.agents/skills/playwright-best-practices/debugging/error-testing.md +0 -360
  40. package/.agents/skills/playwright-best-practices/debugging/flaky-tests.md +0 -496
  41. package/.agents/skills/playwright-best-practices/frameworks/angular.md +0 -530
  42. package/.agents/skills/playwright-best-practices/frameworks/nextjs.md +0 -469
  43. package/.agents/skills/playwright-best-practices/frameworks/react.md +0 -531
  44. package/.agents/skills/playwright-best-practices/frameworks/vue.md +0 -574
  45. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/ci-cd.md +0 -468
  46. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/docker.md +0 -283
  47. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/github-actions.md +0 -546
  48. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/gitlab.md +0 -397
  49. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/other-providers.md +0 -521
  50. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/parallel-sharding.md +0 -371
  51. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/performance.md +0 -453
  52. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/reporting.md +0 -424
  53. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/test-coverage.md +0 -497
  54. package/.agents/skills/playwright-best-practices/testing-patterns/accessibility.md +0 -359
  55. package/.agents/skills/playwright-best-practices/testing-patterns/api-testing.md +0 -719
  56. package/.agents/skills/playwright-best-practices/testing-patterns/browser-extensions.md +0 -506
  57. package/.agents/skills/playwright-best-practices/testing-patterns/canvas-webgl.md +0 -493
  58. package/.agents/skills/playwright-best-practices/testing-patterns/component-testing.md +0 -500
  59. package/.agents/skills/playwright-best-practices/testing-patterns/drag-drop.md +0 -576
  60. package/.agents/skills/playwright-best-practices/testing-patterns/electron.md +0 -509
  61. package/.agents/skills/playwright-best-practices/testing-patterns/file-operations.md +0 -377
  62. package/.agents/skills/playwright-best-practices/testing-patterns/file-upload-download.md +0 -562
  63. package/.agents/skills/playwright-best-practices/testing-patterns/forms-validation.md +0 -561
  64. package/.agents/skills/playwright-best-practices/testing-patterns/graphql-testing.md +0 -331
  65. package/.agents/skills/playwright-best-practices/testing-patterns/i18n.md +0 -508
  66. package/.agents/skills/playwright-best-practices/testing-patterns/performance-testing.md +0 -476
  67. package/.agents/skills/playwright-best-practices/testing-patterns/security-testing.md +0 -430
  68. package/.agents/skills/playwright-best-practices/testing-patterns/visual-regression.md +0 -634
  69. package/.agents/skills/security-qa/SKILL.md +0 -254
  70. package/public/ArcalityImagen.png +0 -0
  71. package/public/logo-arcadial-blanco.png +0 -0
  72. package/public/logo.png +0 -0
  73. package/tests/_helpers/ArcalityReporter.ts +0 -733
  74. package/tests/_helpers/agentic-runner.spec.ts +0 -783
  75. package/tests/_helpers/ai-agent-helper.ts +0 -1660
  76. package/tests/_helpers/discover-view.spec.ts +0 -238
  77. package/tests/_helpers/extract-view.spec.ts +0 -118
  78. package/tests/_helpers/qa-security-tools.ts +0 -265
  79. package/tests/_helpers/qa-tools.ts +0 -333
  80. package/tests/_helpers/smart-action.spec.ts +0 -1458
@@ -1,238 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import 'dotenv/config';
4
- import { test, expect } from '@playwright/test';
5
-
6
- test('discover visible components on view', async ({ page }) => {
7
- const target = process.env.TARGET_PATH || '/';
8
- const base = process.env.BASE_URL || '';
9
-
10
- // Perform canonical login if credentials available
11
- const user = process.env.LOGIN_USER || '';
12
- const pass = process.env.LOGIN_PASSWORD || '';
13
- if (user && pass) {
14
- const loginUrl = base + '/login';
15
- console.log('Logging in via', loginUrl);
16
- await page.goto(loginUrl);
17
-
18
- // Generic selector strategies
19
- const userSelectors = [
20
- 'input[type="email"]', 'input[name="email"]', 'input[name="username"]',
21
- 'input[name="user"]', 'input[name="login"]', 'input[id="email"]',
22
- 'input[id="username"]', 'input[placeholder*="user" i]',
23
- 'input[placeholder*="usuario" i]', 'input[placeholder*="mail" i]'
24
- ];
25
- const passSelectors = [
26
- 'input[type="password"]', 'input[name="password"]',
27
- 'input[name="pass"]', 'input[id="password"]',
28
- 'input[placeholder*="pass" i]', 'input[placeholder*="contra" i]'
29
- ];
30
- const submitSelectors = [
31
- 'button[type="submit"]', 'input[type="submit"]',
32
- 'button:has-text("Login")', 'button:has-text("Sign in")',
33
- 'button:has-text("Entrar")', 'button:has-text("Ingresar")',
34
- 'button:has-text("Iniciar")', 'button:has-text("Acceder")',
35
- '[role="button"]:has-text("Login")', '[role="button"]:has-text("Entrar")'
36
- ];
37
-
38
- let email = null;
39
- for (const s of userSelectors) {
40
- const loc = page.locator(s).first();
41
- if (await loc.isVisible().catch(() => false)) {
42
- email = loc;
43
- console.log('Found user field:', s);
44
- break;
45
- }
46
- }
47
-
48
- let password = null;
49
- for (const s of passSelectors) {
50
- const loc = page.locator(s).first();
51
- if (await loc.isVisible().catch(() => false)) {
52
- password = loc;
53
- console.log('Found password field:', s);
54
- break;
55
- }
56
- }
57
-
58
- let submit = null;
59
- for (const s of submitSelectors) {
60
- const loc = page.locator(s).first();
61
- if (await loc.isVisible().catch(() => false)) {
62
- submit = loc;
63
- console.log('Found submit button:', s);
64
- break;
65
- }
66
- }
67
-
68
- if (email && password && submit) {
69
- await email.fill(user);
70
- await password.fill(pass);
71
- await submit.click();
72
- await expect(page).not.toHaveURL(/\/login(\b|$)/i, { timeout: 15000 }).catch(e => console.warn('Login URL check timeout/fail, continuing...'));
73
- try {
74
- await page.locator('nav, aside, [role="navigation"], [data-testid="sidebar"]').first().waitFor({ state: 'visible', timeout: 5000 });
75
- } catch { }
76
- } else {
77
- console.warn('Could not detect all login fields (user, password, submit) - skipping auto-login step.');
78
- }
79
- }
80
-
81
- const url = base + target;
82
- console.log('Navigating to', url);
83
- await page.goto(url);
84
- await page.waitForLoadState('networkidle');
85
-
86
- // Optionally wait for main container if present
87
- try {
88
- await page.locator('main, [role="main"]').first().waitFor({ state: 'visible', timeout: 3000 });
89
- } catch { }
90
-
91
- // Gather visible interactive elements and some metadata
92
- const components = await page.evaluate(() => {
93
- function isVisible(el: any) {
94
- if (!el) return false;
95
- const style = window.getComputedStyle(el);
96
- if (style && (style.visibility === 'hidden' || style.display === 'none' || +style.opacity === 0)) return false;
97
- const rect = el.getBoundingClientRect();
98
- return rect.width > 0 && rect.height > 0;
99
- }
100
-
101
- function cssPath(el: any) {
102
- if (!(el instanceof Element)) return '';
103
- const path = [];
104
- while (el && el.nodeType === Node.ELEMENT_NODE && el.tagName.toLowerCase() !== 'html') {
105
- let selector = el.tagName.toLowerCase();
106
- if (el.id) selector += `#${el.id}`;
107
- else {
108
- const cls = Array.from(el.classList || []).slice(0, 3).map((c: any) => c.replace(/[^a-z0-9_-]/gi, '')).join('.');
109
- if (cls) selector += `.${cls}`;
110
- }
111
- const siblingIndex = Array.from(el.parentNode ? el.parentNode.children : []).filter((x: any) => x.tagName === el.tagName).indexOf(el) + 1;
112
- if (siblingIndex > 1) selector += `:nth-of-type(${siblingIndex})`;
113
- path.unshift(selector);
114
- el = el.parentElement;
115
- }
116
- return path.join(' > ');
117
- }
118
-
119
- function nearestHeading(el: any) {
120
- let cur = el;
121
- while (cur) {
122
- const headings = Array.from(cur.querySelectorAll('h1,h2,h3,h4,h5,h6')) as any[];
123
- const h = headings.find((x) => x && x.textContent && x.textContent.trim());
124
- if (h) return (h.textContent as string).trim().slice(0, 200);
125
- cur = cur.parentElement;
126
- }
127
- return '';
128
- }
129
-
130
- function nearestLabelForInput(el: any) {
131
- if (!el) return '';
132
- if (el.id) {
133
- const lbl = document.querySelector(`label[for="${el.id}"]`) as any;
134
- if (lbl && lbl.textContent) return (lbl.textContent as string).trim().slice(0, 200);
135
- }
136
- // find closest preceding label sibling
137
- let cur = el.previousElementSibling as any;
138
- while (cur) {
139
- if (cur.tagName?.toLowerCase() === 'label' && cur.textContent) return (cur.textContent as string).trim().slice(0, 200);
140
- cur = cur.previousElementSibling;
141
- }
142
- // try parent label
143
- const parentLabel = el.closest('label') as any;
144
- if (parentLabel && parentLabel.textContent) return (parentLabel.textContent as string).trim().slice(0, 200);
145
- return '';
146
- }
147
-
148
- function attributesMap(el: any) {
149
- const out: Record<string, string> = {};
150
- if (!(el instanceof Element)) return out;
151
- for (const a of Array.from(el.attributes || [])) {
152
- out[a.name] = a.value;
153
- }
154
- return out;
155
- }
156
-
157
- const nodes = Array.from(document.querySelectorAll('a, button, input, textarea, select, [role], label, h1,h2,h3,h4,h5,h6'))
158
- .filter(isVisible);
159
-
160
- const list = nodes.map((el: any) => {
161
- const tag = el.tagName.toLowerCase();
162
- const attrs = attributesMap(el);
163
- const text = (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 400);
164
- const aria = attrs['aria-label'] || attrs['aria-labelledby'] || '';
165
- const placeholder = attrs['placeholder'] || '';
166
- const id = el.id || '';
167
- const name = attrs['name'] || '';
168
- const rect = el.getBoundingClientRect();
169
- const parentText = (el.parentElement && el.parentElement.textContent)
170
- ? (el.parentElement.textContent as string).trim().replace(/\s+/g, ' ').slice(0, 200)
171
- : '';
172
-
173
- return {
174
- tag,
175
- id,
176
- name,
177
- type: attrs['type'] || '',
178
- text,
179
- aria,
180
- placeholder,
181
- attributes: attrs,
182
- data: Object.keys(attrs).filter(k => k.startsWith('data-')).reduce((acc, k) => { acc[k] = attrs[k]; return acc; }, {} as Record<string, string>),
183
- boundingRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
184
- nearestHeading: nearestHeading(el),
185
- parentText,
186
- label: (tag === 'input' || tag === 'textarea' || tag === 'select') ? nearestLabelForInput(el) : '',
187
- selector: cssPath(el),
188
- };
189
- });
190
-
191
- return { timestamp: Date.now(), url: location.href, components: list };
192
- });
193
-
194
- const outDir = process.env.DOMAIN_DIR || path.join(process.cwd(), 'out');
195
- if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
196
- const filePath = path.join(outDir, `components-${Date.now()}.json`);
197
- fs.writeFileSync(filePath, JSON.stringify({ url, timestamp: new Date().toISOString(), components }, null, 2), 'utf8');
198
-
199
- console.log('Wrote components list to', filePath, 'found', components.components.length, 'items');
200
-
201
- // Optional: autofill fields based on discovered components
202
- if (process.env.AUTOFILL === 'true') {
203
- console.log('AUTOFILL enabled — filling detected fields (skipping login fields)');
204
- for (const c of (components as any).components) {
205
- try {
206
- // Skip login fields
207
- if (c.selector && /input\[type="email"\]|input\[type="password"\]/i.test(c.selector)) continue;
208
-
209
- const locator = page.locator(c.selector);
210
- if ((await locator.count()) === 0) continue;
211
-
212
- if (c.tag === 'input') {
213
- const t = (c.type || '').toLowerCase();
214
- if (t === 'email') await locator.fill(`test+${Date.now()}@example.com`).catch(() => { });
215
- else if (t === 'password') await locator.fill(`P@ssw0rd!${Math.floor(Math.random() * 90000)}`).catch(() => { });
216
- else if (t === 'tel') await locator.fill('5551234567').catch(() => { });
217
- else if (t === 'number') await locator.fill('1').catch(() => { });
218
- else await locator.fill('sample text').catch(() => { });
219
- } else if (c.tag === 'textarea') {
220
- await locator.fill('sample text').catch(() => { });
221
- } else if (c.tag === 'select') {
222
- // try to select a non-empty option
223
- await locator.selectOption({ index: 1 }).catch(() => { });
224
- }
225
- } catch (e) {
226
- // ignore individual fill errors
227
- }
228
- }
229
-
230
- if (process.env.AUTOSUBMIT === 'true') {
231
- console.log('AUTOSUBMIT enabled — attempting to submit form');
232
- try {
233
- const submit = page.locator('button[type="submit"], input[type="submit"]').first();
234
- if (await submit.count()) await submit.click().catch(() => { });
235
- } catch { }
236
- }
237
- }
238
- });
@@ -1,118 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import 'dotenv/config';
4
- import { test, expect } from '@playwright/test';
5
-
6
- test('extract view DOM and accessibility snapshot', async ({ page }) => {
7
- const target = process.env.TARGET_PATH || '/';
8
- const base = process.env.BASE_URL || '';
9
-
10
- // Perform canonical login if credentials available
11
- const user = process.env.LOGIN_USER || '';
12
- const pass = process.env.LOGIN_PASSWORD || '';
13
- if (user && pass) {
14
- const loginUrl = base + '/login';
15
- console.log('Logging in via', loginUrl);
16
- await page.goto(loginUrl);
17
-
18
- // Generic selector strategies
19
- const userSelectors = [
20
- 'input[type="email"]', 'input[name="email"]', 'input[name="username"]',
21
- 'input[name="user"]', 'input[name="login"]', 'input[id="email"]',
22
- 'input[id="username"]', 'input[placeholder*="user" i]',
23
- 'input[placeholder*="usuario" i]', 'input[placeholder*="mail" i]'
24
- ];
25
- const passSelectors = [
26
- 'input[type="password"]', 'input[name="password"]',
27
- 'input[name="pass"]', 'input[id="password"]',
28
- 'input[placeholder*="pass" i]', 'input[placeholder*="contra" i]'
29
- ];
30
- const submitSelectors = [
31
- 'button[type="submit"]', 'input[type="submit"]',
32
- 'button:has-text("Login")', 'button:has-text("Sign in")',
33
- 'button:has-text("Entrar")', 'button:has-text("Ingresar")',
34
- 'button:has-text("Iniciar")', 'button:has-text("Acceder")',
35
- '[role="button"]:has-text("Login")', '[role="button"]:has-text("Entrar")'
36
- ];
37
-
38
- let email = null;
39
- for (const s of userSelectors) {
40
- const loc = page.locator(s).first();
41
- if (await loc.isVisible().catch(() => false)) {
42
- email = loc;
43
- console.log('Found user field:', s);
44
- break;
45
- }
46
- }
47
-
48
- let password = null;
49
- for (const s of passSelectors) {
50
- const loc = page.locator(s).first();
51
- if (await loc.isVisible().catch(() => false)) {
52
- password = loc;
53
- console.log('Found password field:', s);
54
- break;
55
- }
56
- }
57
-
58
- let submit = null;
59
- for (const s of submitSelectors) {
60
- const loc = page.locator(s).first();
61
- if (await loc.isVisible().catch(() => false)) {
62
- submit = loc;
63
- console.log('Found submit button:', s);
64
- break;
65
- }
66
- }
67
-
68
- if (email && password && submit) {
69
- await email.fill(user);
70
- await password.fill(pass);
71
- await submit.click();
72
- await expect(page).not.toHaveURL(/\/login(\b|$)/i, { timeout: 15000 }).catch(e => console.warn('Login URL check timeout/fail, continuing...'));
73
- try {
74
- await page.locator('nav, aside, [role="navigation"], [data-testid="sidebar"]').first().waitFor({ state: 'visible', timeout: 5000 });
75
- } catch { }
76
- } else {
77
- console.warn('Could not detect all login fields (user, password, submit) - skipping auto-login step.');
78
- }
79
- }
80
-
81
- const url = base + target;
82
- console.log('Navigating to', url);
83
- await page.goto(url);
84
- await page.waitForLoadState('networkidle');
85
-
86
- // Capture full HTML
87
- const html = await page.content();
88
-
89
- // Capture accessibility tree (helps detect components/roles/labels)
90
- // Note: page.accessibility was deprecated/removed in Playwright 1.38+
91
- let accessibility = null;
92
- try {
93
- accessibility = await (page as any).accessibility?.snapshot({ interestingOnly: false });
94
- } catch (e: any) {
95
- console.warn('Accessibility snapshot failed:', e?.message ?? e);
96
- }
97
-
98
- const outDir = process.env.DOMAIN_DIR || path.join(process.cwd(), 'out');
99
- if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
100
-
101
- const filePath = path.join(outDir, `view-snapshot-${Date.now()}.json`);
102
- fs.writeFileSync(
103
- filePath,
104
- JSON.stringify(
105
- {
106
- url,
107
- timestamp: new Date().toISOString(),
108
- html,
109
- accessibility,
110
- },
111
- null,
112
- 2
113
- ),
114
- 'utf8'
115
- );
116
-
117
- console.log('Wrote view snapshot to', filePath);
118
- });
@@ -1,265 +0,0 @@
1
- /* eslint-disable no-console */
2
- import { Page, expect } from '@playwright/test';
3
-
4
- export type Severity = 'Critical' | 'High' | 'Medium' | 'Low' | 'Info';
5
-
6
- export type SecurityFindingType = 'confirmed_vulnerability' | 'security_misconfiguration' | 'potential_issue' | 'informational';
7
- export type Confidence = 'High' | 'Medium' | 'Low';
8
- export type Exploitability = 'confirmed' | 'likely' | 'unknown' | 'not_confirmed';
9
-
10
- export interface SecurityFinding {
11
- type: SecurityFindingType;
12
- category: string;
13
- subcategory?: string;
14
- severity: Severity;
15
- confidence: Confidence;
16
- exploitability: Exploitability;
17
- status: 'detected';
18
- description: string;
19
- impact?: string;
20
- evidence: {
21
- [key: string]: any;
22
- };
23
- reproductionSteps?: string[];
24
- remediation: string;
25
- classification?: {
26
- owaspTop10?: string;
27
- cwe?: string;
28
- };
29
- }
30
-
31
- /**
32
- * Attempts to inject a simple XSS payload into a specified element and checks for an alert.
33
- * @param page - The Playwright Page object.
34
- * @param selector - The selector for the input field to test.
35
- * @returns A vulnerability object if successful, null otherwise.
36
- */
37
- export async function test_xss_injection(
38
- page: Page,
39
- selector: string,
40
- ): Promise<SecurityFinding | null> {
41
- const payload = `<script>alert('XSS-ARCALITY-TEST')</script>`;
42
- console.log(`[QA-SEC] Testing XSS on selector: ${selector}`);
43
-
44
- let alertTriggered = false;
45
- page.once('dialog', async (dialog) => {
46
- if (dialog.message().includes('XSS-ARCALITY-TEST')) {
47
- alertTriggered = true;
48
- }
49
- await dialog.dismiss();
50
- });
51
-
52
- await page.fill(selector, payload);
53
- // Submit the form or trigger the action that would process the input
54
- // This part is crucial and might need to be adapted based on the application's behavior
55
- await page.press(selector, 'Enter');
56
-
57
- // Wait a moment for any potential script execution
58
- await page.waitForTimeout(1000);
59
-
60
- if (alertTriggered) {
61
- console.log(`[QA-SEC] XSS Vulnerability DETECTED at ${selector}`);
62
- return {
63
- type: 'confirmed_vulnerability',
64
- category: 'Injection',
65
- subcategory: 'Reflected XSS',
66
- severity: 'High',
67
- confidence: 'High',
68
- exploitability: 'confirmed',
69
- status: 'detected',
70
- description: `A reflected Cross-Site Scripting (XSS) vulnerability was detected in the element '${selector}'. The application executed a script injected into this input field.`,
71
- impact: 'Allows execution of arbitrary JavaScript in the context of the user session, potentially leading to session hijacking, credential theft, or unauthorized actions.',
72
- evidence: {
73
- payload,
74
- url: page.url(),
75
- selector,
76
- },
77
- reproductionSteps: [
78
- `Navigate to the page containing '${selector}'.`,
79
- `Inject the payload: ${payload}`,
80
- 'Trigger the form submission or blur event.',
81
- 'Observe the script execution (alert dialog).'
82
- ],
83
- remediation: 'Sanitize all user-supplied input on the server-side before rendering it back to the page. Use libraries like DOMPurify on the client-side as an additional layer of defense.',
84
- classification: {
85
- owaspTop10: 'A03:2021 Injection',
86
- cwe: 'CWE-79'
87
- }
88
- };
89
- }
90
-
91
- console.log(`[QA-SEC] No XSS detected for selector: ${selector}`);
92
- return null;
93
- }
94
-
95
- /**
96
- * Checks if a given URL, assumed to be protected, can be accessed without authentication.
97
- * @param page - The Playwright Page object.
98
- * @param url - The protected URL to test.
99
- * @param redirectUrl - The URL the app should redirect to if unauthorized.
100
- * @returns A vulnerability object if bypass is successful, null otherwise.
101
- */
102
- export async function test_auth_bypass(
103
- page: Page,
104
- url: string,
105
- redirectUrl: string | RegExp,
106
- ): Promise<SecurityFinding | null> {
107
- console.log(`[QA-SEC] Testing Auth Bypass on URL: ${url}`);
108
- const response = await page.goto(url, { waitUntil: 'domcontentloaded' });
109
-
110
- // If the page loads the protected content (doesn't redirect or redirects to a different page)
111
- if (response?.ok() && !page.url().match(redirectUrl)) {
112
- console.log(`[QA-SEC] Auth Bypass DETECTED for URL: ${url}`);
113
- return {
114
- type: 'confirmed_vulnerability',
115
- category: 'Broken Access Control',
116
- subcategory: 'Authentication Bypass',
117
- severity: 'Critical',
118
- confidence: 'High',
119
- exploitability: 'confirmed',
120
- status: 'detected',
121
- description: `Unauthorized access to the protected route '${url}' was successful. The application failed to enforce authentication, allowing a direct bypass to sensitive content.`,
122
- impact: 'An unauthenticated attacker can access protected resources, potentially reading or modifying sensitive user data.',
123
- evidence: {
124
- accessedUrl: url,
125
- finalUrl: page.url(),
126
- },
127
- reproductionSteps: [
128
- 'Ensure no active session (cleared cookies/storage).',
129
- `Navigate directly to: ${url}`,
130
- 'Observe that the protected page loads without redirection.'
131
- ],
132
- remediation: 'Implement server-side middleware to verify user authentication and authorization status before serving any protected routes.',
133
- classification: {
134
- owaspTop10: 'A01:2021 Broken Access Control',
135
- cwe: 'CWE-285'
136
- }
137
- };
138
- }
139
-
140
- console.log(`[QA-SEC] No Auth Bypass detected for URL: ${url}`);
141
- return null;
142
- }
143
-
144
- /**
145
- * Audits the HTTP security headers of the current page.
146
- * @param page - The Playwright Page object.
147
- * @returns An array of vulnerability objects for missing headers.
148
- */
149
- export async function audit_http_headers(page: Page): Promise<SecurityFinding[]> {
150
- console.log(`[QA-SEC] Auditing HTTP Headers for: ${page.url()}`);
151
- const response = await page.reload({ waitUntil: 'domcontentloaded' });
152
- const headers = response?.headers();
153
- const vulnerabilities: SecurityFinding[] = [];
154
-
155
- const securityHeaders = {
156
- 'Content-Security-Policy': {
157
- severity: 'Medium',
158
- remediation: 'Implement a strict Content-Security-Policy (CSP) to mitigate XSS and other injection attacks. Start with a restrictive policy and gradually allow trusted sources.',
159
- impact: 'If an injection issue exists elsewhere in the application, the lack of CSP may increase exploitability and impact.',
160
- cwe: 'CWE-16'
161
- },
162
- 'Strict-Transport-Security': {
163
- severity: 'Medium',
164
- remediation: 'Implement HTTP Strict Transport Security (HSTS) to force browsers to communicate over HTTPS, preventing downgrade attacks.',
165
- impact: 'Man-in-the-middle attacks could downgrade the connection to HTTP, allowing traffic interception.',
166
- cwe: 'CWE-319'
167
- },
168
- 'X-Content-Type-Options': {
169
- severity: 'Low',
170
- remediation: 'Set `X-Content-Type-Options: nosniff` to prevent browsers from MIME-sniffing a response away from the declared content-type.',
171
- impact: 'Browsers may interpret non-executable MIME types as executable, leading to potential XSS.',
172
- cwe: 'CWE-430'
173
- },
174
- 'X-Frame-Options': {
175
- severity: 'Medium',
176
- remediation: 'Set `X-Frame-Options: SAMEORIGIN` or `DENY` to protect against clickjacking attacks by preventing the page from being loaded in an iframe on other domains.',
177
- impact: 'Attackers could trick users into clicking visually hidden elements, causing unintended actions (Clickjacking).',
178
- cwe: 'CWE-1021'
179
- }
180
- };
181
-
182
- for (const [header, info] of Object.entries(securityHeaders)) {
183
- if (!headers || !Object.keys(headers).find(h => h.toLowerCase() === header.toLowerCase())) {
184
- vulnerabilities.push({
185
- type: 'security_misconfiguration',
186
- category: 'Missing Security Header',
187
- subcategory: header,
188
- severity: info.severity as Severity,
189
- confidence: 'High',
190
- exploitability: 'not_confirmed',
191
- status: 'detected',
192
- description: `The '${header}' HTTP header is missing. This header is crucial for defending against various web attacks.`,
193
- impact: info.impact,
194
- evidence: {
195
- url: page.url(),
196
- missingHeader: header
197
- },
198
- reproductionSteps: [
199
- `Navigate to ${page.url()}`,
200
- `Inspect the response headers.`,
201
- `Verify that ${header} is absent.`
202
- ],
203
- remediation: info.remediation,
204
- classification: {
205
- owaspTop10: 'A05:2021 Security Misconfiguration',
206
- cwe: info.cwe
207
- }
208
- });
209
- }
210
- }
211
-
212
- console.log(`[QA-SEC] Found ${vulnerabilities.length} missing security headers.`);
213
- return vulnerabilities;
214
- }
215
-
216
- /**
217
- * Scans localStorage and sessionStorage for potentially sensitive data.
218
- * @param page - The Playwright Page object.
219
- * @returns An array of vulnerability objects for any sensitive data found.
220
- */
221
- export async function scan_sensitive_data_exposure(page: Page): Promise<SecurityFinding[]> {
222
- console.log(`[QA-SEC] Scanning localStorage and sessionStorage for sensitive data.`);
223
- const vulnerabilities: SecurityFinding[] = [];
224
- const sensitiveKeywords = ['token', 'password', 'secret', 'key', 'jwt', 'auth'];
225
-
226
- const storages = {
227
- localStorage: await page.evaluate(() => Object.entries(window.localStorage)),
228
- sessionStorage: await page.evaluate(() => Object.entries(window.sessionStorage)),
229
- };
230
-
231
- for (const [storageType, items] of Object.entries(storages)) {
232
- for (const [key, value] of items) {
233
- if (sensitiveKeywords.some(keyword => key.toLowerCase().includes(keyword))) {
234
- vulnerabilities.push({
235
- type: 'potential_issue',
236
- category: 'Sensitive Data Exposure',
237
- subcategory: 'Browser Storage',
238
- severity: 'Medium',
239
- confidence: 'Medium',
240
- exploitability: 'unknown',
241
- status: 'detected',
242
- description: `Potentially sensitive data was found in ${storageType}. Storing sensitive information like tokens or keys in browser storage is insecure.`,
243
- impact: 'If an attacker discovers an XSS vulnerability, they can read localStorage/sessionStorage and exfiltrate secrets.',
244
- evidence: {
245
- storage: storageType,
246
- key: key,
247
- valuePreview: value ? `${value.substring(0, 10)}...` : '(empty)',
248
- },
249
- reproductionSteps: [
250
- `Open Developer Tools on ${page.url()}`,
251
- `Check ${storageType} for the key '${key}'.`
252
- ],
253
- remediation: 'Avoid storing sensitive data in browser storage. Use secure, HttpOnly cookies for session tokens. If you must store data, encrypt it first.',
254
- classification: {
255
- owaspTop10: 'A04:2021 Insecure Design',
256
- cwe: 'CWE-312'
257
- }
258
- });
259
- }
260
- }
261
- }
262
-
263
- console.log(`[QA-SEC] Found ${vulnerabilities.length} instances of sensitive data in browser storage.`);
264
- return vulnerabilities;
265
- }