@ojas-sta/qalify-plus 1.1.0
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/.github/workflows/npm-publish.yml +52 -0
- package/README.md +106 -0
- package/chrome-extension/CHROMEWEBSTORE.md +50 -0
- package/chrome-extension/background.js +3 -0
- package/chrome-extension/manifest.json +21 -0
- package/chrome-extension/sidepanel.html +49 -0
- package/chrome-extension/sidepanel.js +127 -0
- package/feature_timeline.md +19 -0
- package/package.json +30 -0
- package/src/ai.js +155 -0
- package/src/analyzer.js +93 -0
- package/src/browser.js +229 -0
- package/src/generic.js +304 -0
- package/src/main.js +111 -0
- package/src/ocr.js +78 -0
- package/test/quiz.html +156 -0
- package/test-canvas.html +47 -0
package/src/main.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
require('dotenv').config();
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const BrowserLayer = require('./browser');
|
|
5
|
+
const OCRLayer = require('./ocr');
|
|
6
|
+
const AILayer = require('./ai');
|
|
7
|
+
const AnalyzerLayer = require('./analyzer');
|
|
8
|
+
|
|
9
|
+
async function main() {
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const stressMode = args.includes('--stress');
|
|
12
|
+
|
|
13
|
+
console.log("=========================================");
|
|
14
|
+
console.log(" QUIZ AUTOMATION SIMULATOR STARTING ");
|
|
15
|
+
console.log("=========================================");
|
|
16
|
+
if (stressMode) {
|
|
17
|
+
console.log("[!] STRESS MODE ACTIVATED - Simulating Obfuscated DOM");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!process.env.GEMINI_API_KEY) {
|
|
21
|
+
console.warn("\n[WARNING] GEMINI_API_KEY environment variable is not set.");
|
|
22
|
+
console.warn("The AI Reasoning layer will fail. Please set it before running for full functionality.\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const browserLayer = new BrowserLayer();
|
|
26
|
+
const ocrLayer = new OCRLayer();
|
|
27
|
+
const aiLayer = new AILayer();
|
|
28
|
+
const analyzerLayer = new AnalyzerLayer();
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
await browserLayer.init();
|
|
32
|
+
|
|
33
|
+
// Expose chat function to browser overlay
|
|
34
|
+
await browserLayer.setupChatCallback(async (msg) => {
|
|
35
|
+
return await aiLayer.chat(msg);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const targetUrl = `file://${path.resolve(__dirname, '../test/quiz.html')}`;
|
|
39
|
+
await browserLayer.navigateToQuiz(targetUrl, stressMode);
|
|
40
|
+
|
|
41
|
+
let isRunning = true;
|
|
42
|
+
let questionNumber = 1;
|
|
43
|
+
|
|
44
|
+
while (isRunning) {
|
|
45
|
+
console.log(`\n--- Processing Question ${questionNumber} ---`);
|
|
46
|
+
let extractedData = null;
|
|
47
|
+
let extractionMethod = 'None';
|
|
48
|
+
|
|
49
|
+
// 1. Attempt DOM Extraction
|
|
50
|
+
extractedData = await browserLayer.extractFromDOM();
|
|
51
|
+
|
|
52
|
+
if (extractedData) {
|
|
53
|
+
console.log("[SUCCESS] Extracted via DOM.");
|
|
54
|
+
extractionMethod = 'DOM';
|
|
55
|
+
} else {
|
|
56
|
+
console.log("[INFO] DOM Extraction failed or yielded no usable results. Falling back to OCR.");
|
|
57
|
+
|
|
58
|
+
// 2. Fallback to OCR
|
|
59
|
+
const screenshotPath = path.resolve(__dirname, '../screenshot.png');
|
|
60
|
+
await browserLayer.captureScreenshot(screenshotPath);
|
|
61
|
+
|
|
62
|
+
const processedImagePath = path.resolve(__dirname, '../processed_screenshot.png');
|
|
63
|
+
const finalImagePath = await ocrLayer.preprocessImage(screenshotPath, processedImagePath);
|
|
64
|
+
|
|
65
|
+
extractedData = await ocrLayer.extractText(finalImagePath);
|
|
66
|
+
|
|
67
|
+
if (extractedData) {
|
|
68
|
+
console.log("[SUCCESS] Extracted via OCR.");
|
|
69
|
+
extractionMethod = 'OCR';
|
|
70
|
+
} else {
|
|
71
|
+
console.log("[FAILURE] OCR Extraction also failed.");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let aiOutput = null;
|
|
76
|
+
// 3. AI Reasoning Layer
|
|
77
|
+
if (extractedData && extractedData.question) {
|
|
78
|
+
aiOutput = await aiLayer.determineAnswer(extractedData.question, extractedData.options);
|
|
79
|
+
if (aiOutput) {
|
|
80
|
+
console.log(`[AI] Selected Option: ${aiOutput.selectedOption} (${aiOutput.confidenceScore}% confidence)`);
|
|
81
|
+
|
|
82
|
+
// VISUAL LAYER: Show reasoning and click
|
|
83
|
+
await browserLayer.showAIReasoning(aiOutput);
|
|
84
|
+
await browserLayer.clickOption(aiOutput.selectedOption);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 4. Vulnerability Analysis
|
|
89
|
+
analyzerLayer.evaluate(extractedData, extractionMethod, aiOutput, stressMode);
|
|
90
|
+
|
|
91
|
+
// Get the current text before waiting
|
|
92
|
+
const currentContainerText = await browserLayer.getCurrentContainerText();
|
|
93
|
+
|
|
94
|
+
console.log("\n[INFO] AI has answered. You can now chat in the overlay, or click 'Next Question' in the browser to continue...");
|
|
95
|
+
|
|
96
|
+
// Wait for the container text to change
|
|
97
|
+
isRunning = await browserLayer.waitForQuestionChange(currentContainerText);
|
|
98
|
+
questionNumber++;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log("\n[INFO] Quiz loop finished. Printing final report...");
|
|
102
|
+
analyzerLayer.printSummary();
|
|
103
|
+
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error("Critical Error during execution:", error);
|
|
106
|
+
} finally {
|
|
107
|
+
await browserLayer.close();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
main();
|
package/src/ocr.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const Tesseract = require('tesseract.js');
|
|
2
|
+
const Jimp = require('jimp');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
class OCRLayer {
|
|
6
|
+
constructor() {}
|
|
7
|
+
|
|
8
|
+
async preprocessImage(inputPath, outputPath) {
|
|
9
|
+
console.log(`[OCR] Preprocessing image: ${inputPath}`);
|
|
10
|
+
try {
|
|
11
|
+
// Read the image
|
|
12
|
+
const image = await Jimp.read(inputPath);
|
|
13
|
+
|
|
14
|
+
// Convert to grayscale, increase contrast/sharpness for better OCR
|
|
15
|
+
image.greyscale()
|
|
16
|
+
.contrast(0.5)
|
|
17
|
+
.normalize();
|
|
18
|
+
|
|
19
|
+
await image.writeAsync(outputPath);
|
|
20
|
+
return outputPath;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error("[OCR] Image preprocessing failed:", error);
|
|
23
|
+
// Return original if preprocessing fails
|
|
24
|
+
return inputPath;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async extractText(imagePath) {
|
|
29
|
+
console.log(`[OCR] Extracting text from: ${imagePath}`);
|
|
30
|
+
try {
|
|
31
|
+
const result = await Tesseract.recognize(
|
|
32
|
+
imagePath,
|
|
33
|
+
'eng',
|
|
34
|
+
{ logger: m => {} } // suppress verbose logging
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const text = result.data.text;
|
|
38
|
+
return this.parseExtractedText(text);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error("[OCR] Tesseract extraction failed:", error);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
parseExtractedText(rawText) {
|
|
46
|
+
console.log("[OCR] Parsing extracted text...");
|
|
47
|
+
// Simple heuristic to split question from options
|
|
48
|
+
// Assuming options start with A), B), C), D) or A., B., C., D.
|
|
49
|
+
const lines = rawText.split('\n').map(l => l.trim()).filter(l => l.length > 0);
|
|
50
|
+
|
|
51
|
+
let questionLines = [];
|
|
52
|
+
let options = [];
|
|
53
|
+
|
|
54
|
+
const optionRegex = /^[A-D][\)\.]\s*(.*)/i;
|
|
55
|
+
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
const match = line.match(optionRegex);
|
|
58
|
+
if (match) {
|
|
59
|
+
options.push(line);
|
|
60
|
+
} else {
|
|
61
|
+
if (options.length === 0) {
|
|
62
|
+
questionLines.push(line);
|
|
63
|
+
} else {
|
|
64
|
+
// It's a continuation of the last option, or trailing text.
|
|
65
|
+
// For simplicity, append to the last option if there is one.
|
|
66
|
+
options[options.length - 1] += ' ' + line;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
question: questionLines.join(' ').trim(),
|
|
73
|
+
options: options.map(o => o.trim())
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = OCRLayer;
|
package/test/quiz.html
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Sample Quiz</title>
|
|
5
|
+
<style>
|
|
6
|
+
body { font-family: sans-serif; padding: 20px; }
|
|
7
|
+
.question-container { margin-bottom: 20px; border: 1px solid #ccc; padding: 15px; border-radius: 5px; position: relative; }
|
|
8
|
+
.question-text { font-size: 1.2em; font-weight: bold; margin-bottom: 10px; }
|
|
9
|
+
.option { margin-bottom: 5px; cursor: pointer; display: block; }
|
|
10
|
+
.stress-mode-canvas { display: none; }
|
|
11
|
+
#next-btn { padding: 10px 20px; font-size: 1em; cursor: pointer; background-color: #007bff; color: white; border: none; border-radius: 5px; margin-top: 10px; }
|
|
12
|
+
#next-btn:hover { background-color: #0056b3; }
|
|
13
|
+
.completed { color: green; font-weight: bold; font-size: 1.2em; }
|
|
14
|
+
</style>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<h1>Security Assessment Quiz</h1>
|
|
18
|
+
|
|
19
|
+
<div class="question-container" id="q-container">
|
|
20
|
+
<div class="question-text" id="qt-text">Loading...</div>
|
|
21
|
+
<div class="options-list" id="ol-list"></div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<button id="next-btn">Next Question</button>
|
|
25
|
+
|
|
26
|
+
<script>
|
|
27
|
+
const questions = [
|
|
28
|
+
{
|
|
29
|
+
type: 'text',
|
|
30
|
+
q: "What is the primary purpose of Cross-Site Scripting (XSS)?",
|
|
31
|
+
opts: [
|
|
32
|
+
"A) To perform SQL injection",
|
|
33
|
+
"B) To execute arbitrary JavaScript in the victim's browser",
|
|
34
|
+
"C) To compromise the backend database",
|
|
35
|
+
"D) To sniff network traffic"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'text',
|
|
40
|
+
q: "Which of the following prevents Cross-Site Request Forgery (CSRF)?",
|
|
41
|
+
opts: [
|
|
42
|
+
"A) Anti-CSRF Tokens",
|
|
43
|
+
"B) Prepared Statements",
|
|
44
|
+
"C) Input Validation",
|
|
45
|
+
"D) HTTPS"
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: 'canvas',
|
|
50
|
+
q: "Which HTTP header defends against Clickjacking?",
|
|
51
|
+
opts: [
|
|
52
|
+
"A) X-XSS-Protection",
|
|
53
|
+
"B) Content-Security-Policy",
|
|
54
|
+
"C) X-Frame-Options",
|
|
55
|
+
"D) Strict-Transport-Security"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: 'canvas',
|
|
60
|
+
q: "In what phase of a cyber attack does 'reconnaissance' occur?",
|
|
61
|
+
opts: [
|
|
62
|
+
"A) Exploitation",
|
|
63
|
+
"B) Delivery",
|
|
64
|
+
"C) Information Gathering",
|
|
65
|
+
"D) Maintaining Access"
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
let currentIndex = 0;
|
|
71
|
+
|
|
72
|
+
function loadQuestion(index) {
|
|
73
|
+
if (index >= questions.length) {
|
|
74
|
+
document.getElementById('q-container').innerHTML = "<div class='completed' id='qt-text'>Quiz Completed!</div>";
|
|
75
|
+
document.getElementById('next-btn').style.display = 'none';
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const q = questions[index];
|
|
80
|
+
const qContainer = document.getElementById('q-container');
|
|
81
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
82
|
+
const isStress = urlParams.get('stress') === 'true';
|
|
83
|
+
|
|
84
|
+
// If it's a natively OCR-based question (canvas), OR if we're in stress mode.
|
|
85
|
+
if (q.type === 'canvas' || isStress) {
|
|
86
|
+
console.log("Canvas rendering mode activated: forcing OCR fallback...");
|
|
87
|
+
qContainer.className = "container-xyz123"; // obfuscate container class
|
|
88
|
+
|
|
89
|
+
// We draw the question as an image so DOM text extractors fail completely.
|
|
90
|
+
// But we inject invisible radio buttons so the AI's "click simulation" still works.
|
|
91
|
+
let html = `<canvas id="q-canvas" width="600" height="300"></canvas>`;
|
|
92
|
+
|
|
93
|
+
let yPos = 80;
|
|
94
|
+
q.opts.forEach((optText) => {
|
|
95
|
+
const letter = optText.charAt(0);
|
|
96
|
+
// Invisible hit-boxes for the Playwright mouse to target
|
|
97
|
+
html += `<input type="radio" name="q" id="q_${letter}" style="opacity: 0.01; position: absolute; left: 30px; top: ${yPos - 10}px; width: 20px; height: 20px; z-index: 10;">`;
|
|
98
|
+
yPos += 40;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
qContainer.innerHTML = html;
|
|
102
|
+
|
|
103
|
+
// Draw to canvas
|
|
104
|
+
const canvas = document.getElementById('q-canvas');
|
|
105
|
+
const ctx = canvas.getContext('2d');
|
|
106
|
+
|
|
107
|
+
// Draw background noise (makes basic scrapers suffer, but OCR handles it)
|
|
108
|
+
ctx.fillStyle = '#f0f0f0';
|
|
109
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
110
|
+
for(let i=0; i<500; i++) {
|
|
111
|
+
ctx.fillStyle = `rgba(0,0,0,${Math.random() * 0.1})`;
|
|
112
|
+
ctx.fillRect(Math.random()*canvas.width, Math.random()*canvas.height, 2, 2);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
ctx.fillStyle = 'black';
|
|
116
|
+
ctx.font = 'bold 18px sans-serif';
|
|
117
|
+
ctx.fillText(q.q, 20, 40);
|
|
118
|
+
|
|
119
|
+
ctx.font = '16px sans-serif';
|
|
120
|
+
let currentY = 80;
|
|
121
|
+
q.opts.forEach(optText => {
|
|
122
|
+
ctx.beginPath();
|
|
123
|
+
ctx.arc(40, currentY - 5, 8, 0, 2 * Math.PI);
|
|
124
|
+
ctx.stroke();
|
|
125
|
+
ctx.fillText(optText, 60, currentY);
|
|
126
|
+
currentY += 40;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// We still need a hidden way for our `waitForQuestionChange` loop to know it changed.
|
|
130
|
+
// We'll add a hidden metadata div.
|
|
131
|
+
qContainer.innerHTML += `<div id="q-meta" style="display:none;">${q.q}</div>`;
|
|
132
|
+
|
|
133
|
+
} else {
|
|
134
|
+
// Standard DOM text mode
|
|
135
|
+
qContainer.className = "question-container";
|
|
136
|
+
let html = `<div class="question-text" id="qt-text">${q.q}</div>`;
|
|
137
|
+
html += `<div class="options-list" id="ol-list">`;
|
|
138
|
+
q.opts.forEach((optText) => {
|
|
139
|
+
const letter = optText.charAt(0);
|
|
140
|
+
html += `<div class="option" data-val="${letter}"><input type="radio" name="q" id="q_${letter}"><label for="q_${letter}">${optText}</label></div>`;
|
|
141
|
+
});
|
|
142
|
+
html += `</div>`;
|
|
143
|
+
qContainer.innerHTML = html;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
document.getElementById('next-btn').addEventListener('click', () => {
|
|
148
|
+
currentIndex++;
|
|
149
|
+
loadQuestion(currentIndex);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Initial load
|
|
153
|
+
loadQuestion(currentIndex);
|
|
154
|
+
</script>
|
|
155
|
+
</body>
|
|
156
|
+
</html>
|
package/test-canvas.html
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<body>
|
|
4
|
+
<div id="q-container"></div>
|
|
5
|
+
<script>
|
|
6
|
+
const qContainer = document.getElementById('q-container');
|
|
7
|
+
const q = {
|
|
8
|
+
q: "Which HTTP header defends against Clickjacking?",
|
|
9
|
+
opts: [
|
|
10
|
+
"A) X-XSS-Protection",
|
|
11
|
+
"B) Content-Security-Policy",
|
|
12
|
+
"C) X-Frame-Options",
|
|
13
|
+
"D) Strict-Transport-Security"
|
|
14
|
+
]
|
|
15
|
+
};
|
|
16
|
+
let html = `<canvas id="q-canvas" width="600" height="300"></canvas>`;
|
|
17
|
+
let yPos = 80;
|
|
18
|
+
q.opts.forEach((optText) => {
|
|
19
|
+
const letter = optText.charAt(0);
|
|
20
|
+
html += `<input type="radio" name="q" id="q_${letter}" style="opacity: 0.01; position: absolute; left: 30px; top: ${yPos - 10}px; width: 20px; height: 20px; z-index: 10;">`;
|
|
21
|
+
yPos += 40;
|
|
22
|
+
});
|
|
23
|
+
qContainer.innerHTML = html;
|
|
24
|
+
|
|
25
|
+
const canvas = document.getElementById('q-canvas');
|
|
26
|
+
const ctx = canvas.getContext('2d');
|
|
27
|
+
ctx.fillStyle = '#f0f0f0';
|
|
28
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
29
|
+
for(let i=0; i<500; i++) {
|
|
30
|
+
ctx.fillStyle = `rgba(0,0,0,${Math.random() * 0.1})`;
|
|
31
|
+
ctx.fillRect(Math.random()*canvas.width, Math.random()*canvas.height, 2, 2);
|
|
32
|
+
}
|
|
33
|
+
ctx.fillStyle = 'black';
|
|
34
|
+
ctx.font = 'bold 18px sans-serif';
|
|
35
|
+
ctx.fillText(q.q, 20, 40);
|
|
36
|
+
ctx.font = '16px sans-serif';
|
|
37
|
+
let currentY = 80;
|
|
38
|
+
q.opts.forEach(optText => {
|
|
39
|
+
ctx.beginPath();
|
|
40
|
+
ctx.arc(40, currentY - 5, 8, 0, 2 * Math.PI);
|
|
41
|
+
ctx.stroke();
|
|
42
|
+
ctx.fillText(optText, 60, currentY);
|
|
43
|
+
currentY += 40;
|
|
44
|
+
});
|
|
45
|
+
</script>
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|