Skip to content
Open
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
67 changes: 61 additions & 6 deletions common/components/SMSComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
namespace common\components;

use yii\base\Component;
use yii\base\InvalidConfigException;
use yii\httpclient\Client;

/**
Expand All @@ -10,9 +11,62 @@
class SMSComponent extends Component
{
/**
* @var string Variable for test api key to be stored in
* @var string SMS provider endpoint URL
*/
private $apiEndpoint = "http://62.215.226.164/fccsms.aspx";
public $apiEndpoint;

/**
* @var string SMS provider username
*/
public $username;

/**
* @var string SMS provider password
*/
public $password;

/**
* @var string SMS sender name
*/
public $sender;

/**
* @var string SMS provider language flag
*/
public $language = 'L';

/**
* @inheritdoc
*/
public function init()
{
parent::init();

foreach (['apiEndpoint', 'username', 'password', 'sender'] as $attribute) {
if (!is_string($this->$attribute)) {
throw new InvalidConfigException(strtr('"{class}::{attribute}" cannot be empty.', [
'{class}' => static::class,
'{attribute}' => '$' . $attribute
]));
}

$this->$attribute = trim($this->$attribute);

if ($this->$attribute === '') {
throw new InvalidConfigException(strtr('"{class}::{attribute}" cannot be empty.', [
'{class}' => static::class,
'{attribute}' => '$' . $attribute
]));
}
}

if (stripos($this->apiEndpoint, 'https://') !== 0) {
throw new InvalidConfigException(strtr('"{class}::{attribute}" must use HTTPS.', [
'{class}' => static::class,
'{attribute}' => '$apiEndpoint'
]));
}
}

/**
* Send SMS
Expand All @@ -23,12 +77,12 @@ public function sendSms($phone_number, $message)
$phone_number = str_replace(' ', '', $phone_number);

$smsParams = [
"UID" => "usrbawes",
"p" => "bawes1452",
"S" => "Plugn",
"UID" => $this->username,
"p" => $this->password,
"S" => $this->sender,
"G" => $phone_number,
"M" => $message,
"L" => "L",
"L" => $this->language,
];

$client = new Client();
Expand All @@ -37,6 +91,7 @@ public function sendSms($phone_number, $message)
->setMethod('POST')
->setUrl($this->apiEndpoint)
->setData($smsParams)
->setOptions(['timeout' => 10])
->send();
}
}
6 changes: 5 additions & 1 deletion common/config/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@
'class' => 'common\components\JWT'
],
'smsComponent' => [
'class' => 'common\components\SMSComponent'
'class' => 'common\components\SMSComponent',
'apiEndpoint' => getenv('SMS_PROVIDER_ENDPOINT') ?: null,
'username' => getenv('SMS_PROVIDER_USERNAME') ?: null,
'password' => getenv('SMS_PROVIDER_PASSWORD') ?: null,
'sender' => getenv('SMS_PROVIDER_SENDER') ?: null,
],
'jira' => [
'class' => 'common\components\JiraComponent',
Expand Down
16 changes: 15 additions & 1 deletion docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,18 @@ cd console && ../yii algolia/index candidate
```bash
./yii cron/update-candidate-stats
./yii cron/update-company-stats
```
```

## Runtime Service Credentials

### SMS Provider

The SMS component reads provider settings from environment variables. Do not commit provider usernames, passwords, sender accounts, or private provider URLs.

Required variables:
* `SMS_PROVIDER_ENDPOINT` - HTTPS endpoint for the SMS provider API.
* `SMS_PROVIDER_USERNAME` - provider account username.
* `SMS_PROVIDER_PASSWORD` - provider account password.
* `SMS_PROVIDER_SENDER` - approved sender name shown to recipients.

The component fails closed when any required value is missing or when the endpoint is not HTTPS, so provider credentials are not sent over plaintext HTTP.
70 changes: 70 additions & 0 deletions scripts/check-sms-provider-hardening.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Static guard for SMS provider credential configuration."""

from pathlib import Path
import re
import sys


ROOT = Path(__file__).resolve().parents[1]
SMS_COMPONENT = ROOT / "common" / "components" / "SMSComponent.php"
MAIN_CONFIG = ROOT / "common" / "config" / "main.php"


def fail(message):
"""Exit with a consistent check failure message."""
print(f"SMS provider hardening check failed: {message}", file=sys.stderr)
sys.exit(1)


def read_required_text(path, label):
"""Read a required repository file or fail with a clear path."""
if not path.exists():
fail(f"{label} not found at {path}")
return path.read_text()


component = read_required_text(SMS_COMPONENT, "SMSComponent")
config = read_required_text(MAIN_CONFIG, "common config")

if re.search(r"\$apiEndpoint\s*=\s*['\"]https?://", component):
fail("SMSComponent must not hardcode the provider endpoint")

for provider_key in ("UID", "p", "S"):
pattern = rf"['\"]{re.escape(provider_key)}['\"]\s*=>\s*['\"]"
if re.search(pattern, component):
fail(f"SMSComponent must not hardcode provider parameter {provider_key}")

if "https://" not in component or "must use HTTPS" not in component:
fail("SMSComponent must enforce HTTPS transport")

required_env = [
"SMS_PROVIDER_ENDPOINT",
"SMS_PROVIDER_USERNAME",
"SMS_PROVIDER_PASSWORD",
"SMS_PROVIDER_SENDER",
]

for env_name in required_env:
if f"getenv('{env_name}')" not in config:
fail(f"common config must read {env_name} from the runtime environment")

sms_block_match = re.search(
r"'smsComponent'\s*=>\s*\[(?P<body>.*?)^\s*\],",
config,
re.MULTILINE | re.DOTALL,
)
if not sms_block_match:
fail("common config must define the smsComponent block")

sms_block = sms_block_match.group("body")

for option in ("apiEndpoint", "username", "password", "sender"):
literal_pattern = rf"['\"]{option}['\"]\s*=>\s*['\"]"
if re.search(literal_pattern, sms_block):
fail(f"smsComponent option {option} must not use a committed literal")

if re.search(r"['\"]apiEndpoint['\"]\s*=>\s*['\"]http://", sms_block, re.IGNORECASE):
fail("smsComponent must not configure a plaintext HTTP endpoint")

print("SMS provider hardening check passed.")