@arcadialdev/arcality 2.4.29 → 2.4.31
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.
- package/package.json +3 -3
- package/scripts/gen-and-run.mjs +2 -2
- package/scripts/init.mjs +2 -2
- package/scripts/setup.mjs +2 -2
- package/src/arcalityClient.mjs +3 -2
- package/tests/_helpers/agentic-runner.bundle.spec.js +83 -8
- package/public/ArcalityImagen.png +0 -0
- package/public/logo-arcadial-blanco.png +0 -0
- package/public/logo.png +0 -0
- package/scripts/cleanup-qmsdev.mjs +0 -63
- package/scripts/discover-view.mjs +0 -52
- package/scripts/extract-view.mjs +0 -64
- package/scripts/migrate-to-central-out.mjs +0 -157
- package/tests/_helpers/ArcalityReporter.ts +0 -733
- package/tests/_helpers/agentic-runner.spec.ts +0 -783
- package/tests/_helpers/ai-agent-helper.ts +0 -1660
- package/tests/_helpers/discover-view.spec.ts +0 -238
- package/tests/_helpers/extract-view.spec.ts +0 -118
- package/tests/_helpers/qa-security-tools.ts +0 -265
- package/tests/_helpers/qa-tools.ts +0 -333
- 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
|
-
}
|