Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
],
"require": {
"php": "^8.4.0",
"laravel/ai": "^0.8.0"
"laravel/ai": "*"
},
"require-dev": {
"laravel/pint": "^1.29.1",
Expand Down
12 changes: 10 additions & 2 deletions src/InjectionGuard/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@
"homepage": "https://intercept.promptphp.com",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Victor Ukam",
"email": "victorjohnukam@gmail.com",
"homepage": "https://github.com/veeqtoh",
"role": "Developer"
}
],
"require": {
"php": "^8.4.0",
"laravel/ai": "^0.8.0",
"promptphp/intercept-support": "^0.1"
"laravel/ai": "*",
"promptphp/intercept-support": "*"
},
"autoload": {
"psr-4": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,103 @@

namespace PromptPHP\Intercept\InjectionGuard\Tests\Fixtures;

use Laravel\Ai\Contracts\Gateway\TextGateway;
use Laravel\Ai\Contracts\Gateway\StepTextGateway;
use Laravel\Ai\Contracts\Providers\TextProvider;
use Laravel\Ai\Gateway\TextGenerationLoop;
use Laravel\Ai\Prompts\AgentPrompt;
use Laravel\Ai\Responses\AgentResponse;
use Laravel\Ai\Responses\StreamableAgentResponse;
use RuntimeException;

final class PromptInjectionGuardTestProvider implements TextProvider
{
/**
* {@inheritDoc}
*/
public function prompt(AgentPrompt $prompt): AgentResponse
{
throw new RuntimeException('Not used in this test.');
}

/**
* {@inheritDoc}
*/
public function stream(AgentPrompt $prompt): StreamableAgentResponse
{
throw new RuntimeException('Not used in this test.');
}

public function textGateway(): TextGateway
/**
* {@inheritDoc}
*/
public function useTextGateway(StepTextGateway $gateway): self
{
throw new RuntimeException('Not used in this test.');
return $this;
}

public function useTextGateway(TextGateway $gateway): self
/**
* {@inheritDoc}
*/
public function textGenerationLoop(): TextGenerationLoop
{
return $this;
throw new RuntimeException('Not used in this test.');
}

/**
* {@inheritDoc}
*/
public function defaultTextModel(): string
{
return 'test-model';
}

/**
* {@inheritDoc}
*/
public function cheapestTextModel(): string
{
return 'test-cheapest-model';
}

/**
* {@inheritDoc}
*/
public function smartestTextModel(): string
{
return 'test-smartest-model';
}

// ---- Provider interface ----

/**
* {@inheritDoc}
*/
public function name(): string
{
return 'test-provider';
}

/**
* {@inheritDoc}
*/
public function driver(): string
{
return 'test';
}

/**
* {@inheritDoc}
*/
public function providerCredentials(): array
{
return [];
}

/**
* {@inheritDoc}
*/
public function additionalConfiguration(): array
{
return [];
}
}
4 changes: 2 additions & 2 deletions src/PIIRedactor/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
],
"require": {
"php": "^8.4",
"laravel/ai": "^0.8.0",
"promptphp/intercept-support": "^0.1"
"laravel/ai": "*",
"promptphp/intercept-support": "*"
},
"autoload": {
"psr-4": {
Expand Down
34 changes: 23 additions & 11 deletions src/PIIRedactor/src/Detectors/RegexDetector.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace PromptPHP\Intercept\PIIRedactor\Detectors;

use Closure;
use InvalidArgumentException;
use PromptPHP\Intercept\PIIRedactor\Detectors\Contracts\Detector;
use PromptPHP\Intercept\PIIRedactor\ValueObjects\Detection;
Expand All @@ -13,14 +14,16 @@
/**
* Create a new regex detector.
*
* @param string $type The detector entity type.
* @param string $pattern The regex pattern to use for detection.
* @param float $confidence The confidence level of the detection (default: 1.0).
* @param string $type The detector entity type.
* @param string $pattern The regex pattern to use for detection.
* @param float $confidence The confidence level of the detection (default: 1.0).
* @param ?Closure $validator A optional closure to validate detected values.
*/
public function __construct(
protected string $type,
protected string $pattern,
protected float $confidence = 1.0,
protected ?Closure $validator = null,
) {
//
}
Expand All @@ -40,7 +43,7 @@ public function type(): string
*
* @param string $text The text to analyze for sensitive values.
*
* @return array<int, Detection>
* @return array<int, Detection> An array of Detection objects representing the detected sensitive values.
*/
public function detect(string $text): array
{
Expand All @@ -59,15 +62,24 @@ public function detect(string $text): array
return [];
}

return array_map(
fn (array $match): Detection => new Detection(
$detections = [];

foreach ($matches[0] as $match) {
$value = $match[0];

if ($this->validator !== null && ! ($this->validator)($value)) {
continue;
}

$detections[] = new Detection(
type: $this->type,
value: $match[0],
value: $value,
start: $match[1],
length: strlen($match[0]),
length: strlen($value),
confidence: $this->confidence,
),
$matches[0],
);
);
}

return $detections;
}
}
43 changes: 39 additions & 4 deletions src/PIIRedactor/src/PIIRedactor.php
Original file line number Diff line number Diff line change
Expand Up @@ -373,19 +373,53 @@ protected function defaultDetectors(): array
EntityTypes::MAC_ADDRESS->value,
'/\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b/'
),
// Pattern 1: URLs with scheme (http:// or https://).
new RegexDetector(
EntityTypes::URL->value,
'/\b(?:https?:\/\/|www\.)[a-zA-Z0-9+&@#\/%?=~_|!:,.;]*[a-zA-Z0-9+&@#\/%=~_|]|\b(?<![a-zA-Z0-9@])(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?:\/[a-zA-Z0-9+&@#\/%?=~_|!:,.;]*[a-zA-Z0-9+&@#\/%=~_|])?\b/'
'~\bhttps?://[^\s<>"{}|\\^`\[\]]+(?<![.,;:!])~i',
1.0,
function (string $url): bool {
$url = rtrim($url, '.,;:!'); // trim trailing punctuation swallowed by regex.

$parsed = parse_url($url);

return $parsed !== false
&& isset($parsed['scheme'], $parsed['host'])
&& in_array($parsed['scheme'], ['http', 'https'], true);
}
),
// Pattern 2: URLs without scheme (www.example.com or example.com/path)
// More conservative to avoid false positives.
new RegexDetector(
EntityTypes::URL->value,
'~\b(?<![a-zA-Z0-9@])[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?:/[^\s<>"{}|\\^`\[\]]+)?(?<![.,;:!])~i',
0.9,
function (string $url): bool {
// Bare domains in prose are not URLs.
if (! str_contains($url, '/') && ! str_starts_with($url, 'www.')) {
return false;
}

$url = rtrim($url, '.,;:!');

// Prepend scheme so parse_url can validate the host.
$parsed = parse_url('http://'.$url);

return $parsed !== false
&& isset($parsed['host'])
&& str_contains($parsed['host'], '.');
}
),

];
}

/**
* Remove overlapping detections.
*
* @param array<int, Detection> $detections Thre list of detections to process.
* @param array<int, Detection> $detections The list of detections to process.
*
* @return array<int, Detection>
* @return array<int, Detection> The list of detections with overlaps removed.
*/
protected function removeOverlaps(array $detections): array
{
Expand Down Expand Up @@ -439,6 +473,7 @@ protected function priority(string $type): int
EntityTypes::EMAIL->value => 30,
EntityTypes::IP_ADDRESS->value => 20,
EntityTypes::PHONE->value => 10,
EntityTypes::URL->value => 8,
EntityTypes::MAC_ADDRESS->value => 5,
default => 0,
};
Expand Down Expand Up @@ -570,7 +605,7 @@ protected function maskGeneric(string $value): string
*
* @param array<int, Detection> $detections The list of detections to summarise.
*
* @return array<string, int>
* @return array<string, int> A summary of detected entities, with entity types as keys and counts as values.
*/
protected function summariseEntities(array $detections): array
{
Expand Down
Loading
Loading