├── .gitignore ├── Makefile ├── README.md ├── SecurityTxt └── Parser.php ├── composer.json ├── phpunit.xml └── test ├── fixtures └── basic.txt └── fullstack └── BasicTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | vendor 3 | composer.lock 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | ./vendor/bin/phpunit --colors --verbose 3 | 4 | dev-deps: 5 | composer install --dev 6 | 7 | .PHONY: test dev-deps 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP security.txt parser 2 | 3 | Work in progress. 4 | 5 | ## Install 6 | 7 | Install from [packagist](https://packagist.org/packages/tomnomnom/phpsecuritytxt): 8 | 9 | ``` 10 | ▶ composer require tomnomnom/phpsecuritytxt 11 | ``` 12 | 13 | ## Usage 14 | 15 | Parse a `security.txt` file: 16 | ```php 17 | parse($raw); 31 | ``` 32 | 33 | Get contact info: 34 | ```php 35 | contact() as $contact){ 37 | echo "Contact: {$contact}\n"; 38 | } 39 | ``` 40 | 41 | Get encryption info: 42 | ```php 43 | encryption() as $encryption){ 45 | echo "Encryption link: {$encryption}\n"; 46 | } 47 | ``` 48 | 49 | Get acknowledgement info: 50 | ```php 51 | acknowledgement() as $acknowledgement){ 53 | echo "Acknowledgement link: {$acknowledgement}\n"; 54 | } 55 | ``` 56 | 57 | Get parser errors: 58 | ```php 59 | errors() as $error){ 61 | echo "Error: {$error}\n"; 62 | } 63 | ``` 64 | 65 | Get comments: 66 | ```php 67 | comments() as $comment){ 69 | echo "Comment: {$comment}\n"; 70 | } 71 | ``` 72 | 73 | ## TODO 74 | * Add support for fetching URLs directly 75 | * Improve test coverage 76 | * Set up travis to run tests 77 | -------------------------------------------------------------------------------- /SecurityTxt/Parser.php: -------------------------------------------------------------------------------- 1 | [], 14 | self::FIELD_ENCRYPTION => [], 15 | self::FIELD_ACKNOWLEDGEMENT => [], 16 | ]; 17 | 18 | public function __construct($raw = ""){ 19 | if ($raw){ 20 | $this->parse($raw); 21 | } 22 | } 23 | 24 | public function parse($raw){ 25 | $lines = explode("\n", $raw); 26 | 27 | if (sizeOf($lines) < 1){ 28 | $this->addError("empty file"); 29 | return false; 30 | } 31 | 32 | $n = 0; 33 | foreach ($lines as $line){ 34 | $n++; 35 | 36 | // Empty line 37 | $line = trim($line); 38 | if (!$line) continue; 39 | 40 | // Comment 41 | if ($line[0] == "#"){ 42 | $this->comments[] = $line; 43 | continue; 44 | } 45 | 46 | $parts = explode(":", $line, 2); 47 | if (sizeOf($parts) != 2){ 48 | $this->addError("invalid input on line {$n}: {$line}"); 49 | continue; 50 | } 51 | 52 | $option = strToLower($parts[0]); 53 | $value = trim($parts[1]); 54 | 55 | if (!$this->validateField($option, $value, $n)){ 56 | continue; 57 | } 58 | 59 | $this->fields[$option][] = $value; 60 | } 61 | 62 | if (sizeOf($this->fields[self::FIELD_CONTACT]) < 1){ 63 | $this->addError("does not contain at least one contact field"); 64 | return false; 65 | } 66 | 67 | return !$this->hasErrors(); 68 | } 69 | 70 | private function validateField($option, $value, $lineNo = 0){ 71 | switch ($option){ 72 | case self::FIELD_CONTACT: 73 | return $this->validateContact($option, $value, $lineNo); 74 | 75 | case self::FIELD_ENCRYPTION: 76 | case self::FIELD_ACKNOWLEDGEMENT: 77 | return $this->validateUri($option, $value, $lineNo); 78 | 79 | default: 80 | $this->addError("invalid option '{$option}' on line {$lineNo}"); 81 | } 82 | return false; 83 | } 84 | 85 | private function validateContact($option, $value, $lineNo){ 86 | $lower = strToLower($value); 87 | if (!( 88 | filter_var($value, FILTER_VALIDATE_URL) || 89 | filter_var($value, FILTER_VALIDATE_EMAIL) || 90 | $this->isValidPhoneNumber($value) 91 | )){ 92 | $this->addError("invalid value '{$value}' for option '{$option}' on line {$lineNo}"); 93 | return false; 94 | } 95 | return true; 96 | } 97 | 98 | private function validateUri($option, $value, $lineNo){ 99 | if (!filter_var($value, FILTER_VALIDATE_URL)){ 100 | $this->addError("invalid URI '{$value}' for option '{$option}' on line {$lineNo}"); 101 | return false; 102 | } 103 | return true; 104 | } 105 | 106 | private function isValidPhoneNumber($candidate){ 107 | return (preg_match("/^\+[0-9\(\) -]+$/", $candidate) > 0); 108 | } 109 | 110 | private function addError($msg){ 111 | $this->errors[] = $msg; 112 | } 113 | 114 | public function hasErrors(){ 115 | return (sizeOf($this->errors) > 0); 116 | } 117 | 118 | public function errors(){ 119 | return $this->errors; 120 | } 121 | 122 | public function hasComments(){ 123 | return (sizeOf($this->comments) > 0); 124 | } 125 | 126 | public function comments(){ 127 | return $this->comments; 128 | } 129 | 130 | public function hasContact(){ 131 | return (sizeOf($this->fields[self::FIELD_CONTACT]) > 0); 132 | } 133 | 134 | public function contact(){ 135 | return $this->fields[self::FIELD_CONTACT]; 136 | } 137 | 138 | public function hasEncryption(){ 139 | return (sizeOf($this->fields[self::FIELD_ENCRYPTION]) > 0); 140 | } 141 | 142 | public function encryption(){ 143 | return $this->fields[self::FIELD_ENCRYPTION]; 144 | } 145 | 146 | public function hasAcknowledgement(){ 147 | return (sizeOf($this->fields[self::FIELD_ACKNOWLEDGEMENT]) > 0); 148 | } 149 | 150 | public function acknowledgement(){ 151 | return $this->fields[self::FIELD_ACKNOWLEDGEMENT]; 152 | } 153 | 154 | public function fields(){ 155 | return $this->fields; 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tomnomnom/phpsecuritytxt", 3 | "description": "A parser for security.txt files", 4 | "homepage": "https://github.com/tomnomnom/phpsecuritytxt", 5 | "license": "MIT", 6 | 7 | "require-dev": { 8 | "phpunit/phpunit": "3.7.*" 9 | }, 10 | "autoload": { 11 | "psr-4": {"SecurityTxt\\": "SecurityTxt/"} 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | test/fullstack 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/fixtures/basic.txt: -------------------------------------------------------------------------------- 1 | # This is an example file 2 | Contact: mail@tomnomnom.com 3 | Contact: https://tomnomnom.com 4 | 5 | # This field has different casing 6 | acknowledgement: https://tomnomnom.uk/hof 7 | ENCRYPTION: https://tomnomnom.uk/pgpkey 8 | 9 | Encryption: not a URL 10 | Acknowledgement: not a URL 11 | 12 | Contact: +44 7555 555 555 13 | Contact: +44-7555-555-555 14 | 15 | Contact: +44-INVALID-NUMBER 16 | -------------------------------------------------------------------------------- /test/fullstack/BasicTest.php: -------------------------------------------------------------------------------- 1 | parse($raw); 13 | 14 | // The basic file has some errors, so the parse result should be false 15 | $this->assertFalse($r, "expected false result from parse()"); 16 | 17 | $this->assertTrue($s->hasComments()); 18 | $this->assertTrue($s->hasErrors()); 19 | $this->assertTrue($s->hasContact()); 20 | $this->assertTrue($s->hasEncryption()); 21 | $this->assertTrue($s->hasAcknowledgement()); 22 | 23 | $this->assertEquals(2, sizeOf($s->comments())); 24 | $this->assertEquals(3, sizeOf($s->errors())); 25 | $this->assertEquals(4, sizeOf($s->contact())); 26 | $this->assertEquals(1, sizeOf($s->encryption())); 27 | $this->assertEquals(1, sizeOf($s->acknowledgement())); 28 | } 29 | 30 | } 31 | --------------------------------------------------------------------------------