Skip to content

Commit

Permalink
Use regex to check for prompt injection attempts. First code with uni…
Browse files Browse the repository at this point in the history
…t tests
  • Loading branch information
marcusgreen committed Feb 3, 2025
1 parent 7dfab70 commit d8d9754
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 2 deletions.
60 changes: 60 additions & 0 deletions classes/log.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

namespace qtype_aitext;

/**
* Class logging
*
* @package qtype_aitext
* @copyright 2025 Marcus Green
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class log {
public function insert(int $aitextid, string $prompt) :bool {
global $DB, $USER;
if(get_config('qtype_aitext', 'logallprompts') == '1') {
$record = new \stdClass();
$record->aitext = $aitextid;
$record->userid = $USER->id;
$record->prompt = $prompt;
$record->pattern = '';
$record->timeupdated = time();

$DB->insert_record('qtype_aitext_log', $record);
return true;
}

if(get_config('qtype_aitext', 'regularexpressions') == '1') {
$patterns = explode("\n", get_config('qtype_aitext', 'injectionprompts'));
foreach ($patterns as $pattern) {
if (preg_match($pattern, $prompt)) {
// Typically a prompt injection attempt.
$record = new \stdClass();
$record->aitext = $aitextid;
$record->userid = $USER->id;
$record->prompt = $prompt;
$record->pattern = $pattern;
$record->timeupdated = time();
$DB->insert_record('qtype_aitext_log', $record);
return true;
}
}
}
return false;
}

}
14 changes: 14 additions & 0 deletions db/install.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,19 @@
<KEY NAME="questionid" TYPE="foreign-unique" FIELDS="questionid" REFTABLE="question" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="qtype_aitext_log" COMMENT="Logging table for aitext questions">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="aitext" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key to qtype_aitext table"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key to user table"/>
<FIELD NAME="prompt" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="The prompt that was sent to the AI"/>
<FIELD NAME="timeupdated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Timestamp of when the log was created"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="aitext" TYPE="foreign" FIELDS="aitext" REFTABLE="qtype_aitext" REFFIELDS="id"/>
</KEYS>
</TABLE>

</TABLES>
</XMLDB>
25 changes: 25 additions & 0 deletions db/upgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,30 @@ function xmldb_qtype_aitext_upgrade($oldversion) {
upgrade_plugin_savepoint(true, 2024051101, 'qtype', 'aitext');
}

xdebug_break();
if ($oldversion < 2024071802) {

// Define table qtype_aitext_log to be created.
$table = new xmldb_table('qtype_aitext_log');

// Adding fields to table qtype_aitext_log.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('aitext', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('prompt', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null);
$table->add_field('timeupdated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);

// Adding keys to table qtype_aitext_log.
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$table->add_key('aitext', XMLDB_KEY_FOREIGN, ['aitext'], 'qtype_aitext', ['id']);

// Create the table.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}

upgrade_plugin_savepoint(true, 2024071802, 'qtype', 'aitext');
}

return true;
}
12 changes: 11 additions & 1 deletion lang/en/qtype_aitext.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
$string['markscheme_help'] = 'This will tell the AI grader how to give a numerical grade to the student response. The total possible score is this question\'s \'Default mark\'';
$string['markschememissing'] = 'The marke scheme is missing. Please enter a prompt, how to mark the users input';
$string['maxwordlimit'] = 'Maximum word limit';
$string['maxwordlimit_help'] = 'If the response requires that students enter text, this is the maximum number of words that each student will be allowed to submit.';
$string['maxwordlimit_help'] = 'Ifo the response requires that students enter text, this is the maximum number of words that each student will be allowed to submit.';
$string['maxwordlimitboundary'] = 'The word limit for this question is {$a->limit} words and you are attempting to submit {$a->count} words. Please shorten your response and try again.';
$string['minwordlimit'] = 'Minimum word limit';
$string['minwordlimit_help'] = 'If the response requires that students enter text, this is the minimum number of words that each student will be allowed to submit.';
Expand Down Expand Up @@ -121,3 +121,13 @@
$string['wordcount'] = 'Word count: {$a}';
$string['wordcounttoofew'] = 'Word count: {$a->count}, less than the required {$a->limit} words.';
$string['wordcounttoomuch'] = 'Word count: {$a->count}, more than the limit of {$a->limit} words.';

$string['regularexpressions'] = 'Regular expressions';
$string['regularexpressions_setting'] = 'List of regular expressions to define what gets logged, e.g. prompt injection attempts.';

$string['logallprompts'] = 'Log all prompts';
$string['logallprompts_setting'] = 'Enable logging of all AI prompts for monitoring and debugging purposes';

$string['logbyregex'] = 'Log by regular expression';
$string['logbyregex_setting'] = 'Enable logging of prompts that match specified regular expressions (e.g. prompt injection attempts)';

31 changes: 31 additions & 0 deletions settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,37 @@
new lang_string('translatepostfix_text', 'qtype_aitext'),
1
));
$patterns = [
'/ignore previous instructions/i',
'/disregard previous instructions/i',
'/forget previous instructions/i',
'/override previous instructions/i'
];

$settings->add(new admin_setting_configcheckbox(
'qtype_aitext/logallprompts',
new lang_string('logallprompts', 'qtype_aitext'),
new lang_string('logallprompts_setting', 'qtype_aitext'),
0
));

$settings->add(new admin_setting_configcheckbox(
'qtype_aitext/logbyregex',
new lang_string('logbyregex', 'qtype_aitext'),
new lang_string('logbyregex_setting', 'qtype_aitext'),
0
));
$defaultpatterns = implode("\n", $patterns);
$settings->add(new admin_setting_configtextarea(
'qtype_aitext/regularexpressions',
new lang_string('regularexpressions', 'qtype_aitext'),
new lang_string('regularexpressions_setting', 'qtype_aitext'),
$defaultpatterns,
PARAM_RAW,
80,
6
));


}

121 changes: 121 additions & 0 deletions tests/log_test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.


/**
* Tests for AI Text
*
* @package qtype_aitext
* @category test
* @copyright 2025 Marcus Green
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace qtype_aitext;
use qtype_aitext;
use qtype_aitext_test_helper;

defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
require_once($CFG->dirroot . '/question/type/aitext/tests/helper.php');
require_once($CFG->dirroot . '/question/type/aitext/questiontype.php');

class log_test extends \advanced_testcase {
/**
* Always aitext
*
* @var mixed
*/
protected $question;

protected function setUp(): void {
parent::setUp();
// $this->qtype->id = rand(100, 1000);
$this->question = qtype_aitext_test_helper::make_aitext_question([]);
$this->setAdminUser();
}

public function test_insert_all_prompts() {
global $DB;
$this->resetAfterTest(true);

// Set config for logging all prompts
set_config('logallprompts', '1', 'qtype_aitext');

$log = new log();
$prompt = "Test prompt text";

// Test the insert
$result = $log->insert($this->question->id,$prompt);

// // Assert the result is true
$this->assertTrue($result);

// // Verify the database record
$records = $DB->get_records('qtype_aitext_log');
$this->assertCount(1, $records);
}

public function test_insert_injection_detection() {
global $DB;
$this->resetAfterTest(true);


// Set config for regular expressions
set_config('regularexpressions', '1', 'qtype_aitext');
set_config('injectionprompts', '/malicious|hack/', 'qtype_aitext');

$log = new log();
$prompt = "This is a malicious prompt";

// Test the insert
$result = $log->insert($this->question->id,$prompt);

// Assert the result is true
$this->assertTrue($result);

// Verify the database record
$records = $DB->get_records('qtype_aitext_log');
$this->assertCount(1, $records);

// $record = reset($records);
// $this->assertEquals($prompt, $record->prompt);
// $this->assertEquals('/malicious|hack/', $record->pattern);
}

public function test_insert_no_logging() {
global $DB;
$this->resetAfterTest(true);

// Ensure both logging options are disabled
set_config('logallprompts', '0', 'qtype_aitext');
set_config('regularexpressions', '0', 'qtype_aitext');

$log = new log();
$prompt = "Normal prompt text";

// Test the insert
$result = $log->insert($this->question->id,$prompt);

// Assert the result is false
$this->assertFalse($result);

// Verify no records were created
$records = $DB->get_records('qtype_aitext_log');
$this->assertCount(0, $records);
}
}
2 changes: 1 addition & 1 deletion version.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
defined('MOODLE_INTERNAL') || die();

$plugin->component = 'qtype_aitext';
$plugin->version = 2024051110;
$plugin->version = 2024071802;
$plugin->requires = 2020110900;
$plugin->release = '0.02';
$plugin->maturity = MATURITY_BETA;

0 comments on commit d8d9754

Please sign in to comment.