librenms /
librenmsv2
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | <?php |
||
| 2 | /** |
||
| 3 | * DeviceGroup.php |
||
| 4 | * |
||
| 5 | * Dynamic groups of devices |
||
| 6 | * |
||
| 7 | * This program is free software: you can redistribute it and/or modify |
||
| 8 | * it under the terms of the GNU General Public License as published by |
||
| 9 | * the Free Software Foundation, either version 3 of the License, or |
||
| 10 | * (at your option) any later version. |
||
| 11 | * |
||
| 12 | * This program is distributed in the hope that it will be useful, |
||
| 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the |
||
| 15 | * GNU General Public License for more details. |
||
| 16 | * |
||
| 17 | * You should have received a copy of the GNU General Public License |
||
| 18 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
||
| 19 | * |
||
| 20 | * @package LibreNMS |
||
| 21 | * @link http://librenms.org |
||
| 22 | * @copyright 2016 Tony Murray |
||
| 23 | * @author Tony Murray <[email protected]> |
||
| 24 | */ |
||
| 25 | |||
| 26 | namespace App\Models; |
||
| 27 | |||
| 28 | use App\Util; |
||
| 29 | use DB; |
||
| 30 | use Illuminate\Database\Eloquent\Model; |
||
| 31 | use Settings; |
||
| 32 | |||
| 33 | /** |
||
| 34 | * App\Models\DeviceGroup |
||
| 35 | * |
||
| 36 | * @property integer $id |
||
| 37 | * @property string $name |
||
| 38 | * @property string $desc |
||
| 39 | * @property string $pattern |
||
| 40 | * @property array $params |
||
| 41 | * @property string $patternSql |
||
| 42 | * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Device[] $devices |
||
| 43 | * @method static \Illuminate\Database\Query\Builder|\App\Models\DeviceGroup whereId($value) |
||
| 44 | * @method static \Illuminate\Database\Query\Builder|\App\Models\DeviceGroup whereName($value) |
||
| 45 | * @method static \Illuminate\Database\Query\Builder|\App\Models\DeviceGroup whereDesc($value) |
||
| 46 | * @method static \Illuminate\Database\Query\Builder|\App\Models\DeviceGroup wherePattern($value) |
||
| 47 | * @mixin \Eloquent |
||
| 48 | * @property-read mixed $pattern_sql |
||
| 49 | * @property-read mixed $device_count |
||
| 50 | * @method static \Illuminate\Database\Query\Builder|\App\Models\DeviceGroup whereParams($value) |
||
| 51 | */ |
||
| 52 | class DeviceGroup extends Model |
||
| 53 | { |
||
| 54 | /** |
||
| 55 | * Indicates if the model should be timestamped. |
||
| 56 | * |
||
| 57 | * @var bool |
||
| 58 | */ |
||
| 59 | public $timestamps = false; |
||
| 60 | /** |
||
| 61 | * The table associated with the model. |
||
| 62 | * |
||
| 63 | * @var string |
||
| 64 | */ |
||
| 65 | protected $table = 'device_groups'; |
||
| 66 | /** |
||
| 67 | * The primary key column name. |
||
| 68 | * |
||
| 69 | * @var string |
||
| 70 | */ |
||
| 71 | protected $primaryKey = 'id'; |
||
| 72 | /** |
||
| 73 | * Virtual attributes |
||
| 74 | * |
||
| 75 | * @var string |
||
| 76 | */ |
||
| 77 | protected $appends = ['patternSql', 'deviceCount']; |
||
| 78 | |||
| 79 | /** |
||
| 80 | * The attributes that can be mass assigned. |
||
| 81 | * |
||
| 82 | * @var array |
||
| 83 | */ |
||
| 84 | protected $fillable = ['name', 'desc', 'pattern', 'params']; |
||
| 85 | |||
| 86 | /** |
||
| 87 | * The attributes that should be casted to native types. |
||
| 88 | * |
||
| 89 | * @var array |
||
| 90 | */ |
||
| 91 | protected $casts = ['params' => 'array']; |
||
| 92 | |||
| 93 | // ---- Helper Functions ---- |
||
| 94 | |||
| 95 | |||
| 96 | 1 | public function updateRelations() |
|
| 97 | { |
||
| 98 | // we need an id to add relationships |
||
| 99 | 1 | if (is_null($this->id)) { |
|
| 100 | $this->save(); |
||
| 101 | } |
||
| 102 | |||
| 103 | 1 | $device_ids = $this->getDeviceIdsRaw(); |
|
| 104 | |||
| 105 | // update the relationships (deletes and adds as needed) |
||
| 106 | 1 | $this->devices()->sync($device_ids); |
|
| 107 | 1 | } |
|
| 108 | |||
| 109 | /** |
||
| 110 | * Get an array of the device ids from this group by re-querying the database with |
||
| 111 | * either the specified pattern or the saved pattern of this group |
||
| 112 | * |
||
| 113 | * @param string $statement Optional, will use the pattern from this group if not specified |
||
| 114 | * @param array $params array of paremeters |
||
| 115 | * @return array |
||
| 116 | */ |
||
| 117 | 1 | public function getDeviceIdsRaw($statement = null, $params = null) |
|
| 118 | { |
||
| 119 | 1 | if (is_null($statement)) { |
|
| 120 | 1 | $statement = $this->pattern; |
|
| 121 | } |
||
| 122 | |||
| 123 | 1 | if (is_null($params)) { |
|
| 124 | 1 | if (empty($this->params)) { |
|
| 125 | if (!starts_with($statement, '%')) { |
||
| 126 | // can't build sql |
||
| 127 | return []; |
||
| 128 | } |
||
| 129 | } else { |
||
| 130 | 1 | $params = $this->params; |
|
| 131 | } |
||
| 132 | } |
||
| 133 | |||
| 134 | 1 | $statement = $this->applyGroupMacros($statement); |
|
| 135 | 1 | $tables = $this->getTablesFromPattern($statement); |
|
|
0 ignored issues
–
show
|
|||
| 136 | |||
| 137 | 1 | $query = null; |
|
|
0 ignored issues
–
show
$query is not used, you could remove the assignment.
This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently. $myVar = 'Value';
$higher = false;
if (rand(1, 6) > 3) {
$higher = true;
} else {
$higher = false;
}
Both the Loading history...
|
|||
| 138 | 1 | if (count($tables) == 1) { |
|
| 139 | 1 | $query = DB::table($tables[0])->select('device_id')->distinct(); |
|
| 140 | } else { |
||
| 141 | 1 | $query = DB::table('devices')->select('devices.device_id')->distinct(); |
|
| 142 | |||
| 143 | 1 | foreach ($tables as $table) { |
|
| 144 | // skip devices table, we used that as the base. |
||
| 145 | 1 | if ($table == 'devices') { |
|
| 146 | 1 | continue; |
|
| 147 | } |
||
| 148 | |||
| 149 | 1 | $query = $query->join($table, 'devices.device_id', '=', $table.'.device_id'); |
|
| 150 | } |
||
| 151 | } |
||
| 152 | |||
| 153 | // match the device ids |
||
| 154 | 1 | if (is_null($params)) { |
|
| 155 | return $query->whereRaw($statement)->pluck('device_id')->toArray(); |
||
| 156 | } else { |
||
| 157 | 1 | return $query->whereRaw($statement, $params)->pluck('device_id')->toArray(); |
|
| 158 | } |
||
| 159 | } |
||
| 160 | |||
| 161 | /** |
||
| 162 | * Process Macros |
||
| 163 | * |
||
| 164 | * @param string $pattern Rule to process |
||
| 165 | * @param int $x Recursion-Anchor, do not pass |
||
| 166 | * @return string|boolean |
||
| 167 | */ |
||
| 168 | 2 | public static function applyGroupMacros($pattern, $x = 1) |
|
| 169 | { |
||
| 170 | 2 | if (!str_contains($pattern, 'macros.')) { |
|
| 171 | 1 | return $pattern; |
|
| 172 | } |
||
| 173 | |||
| 174 | 1 | foreach (Settings::get('alert.macros.group', []) as $macro => $value) { |
|
| 175 | 1 | $value = str_replace(['%', '&&', '||'], ['', 'AND', 'OR'], $value); // this might need something more complex |
|
| 176 | 1 | if (!str_contains($macro, ' ')) { |
|
| 177 | 1 | $pattern = str_replace('macros.'.$macro, '('.$value.')', $pattern); |
|
| 178 | } |
||
| 179 | } |
||
| 180 | |||
| 181 | 1 | if (str_contains($pattern, 'macros.')) { |
|
| 182 | if (++$x < 30) { |
||
| 183 | $pattern = self::applyGroupMacros($pattern, $x); |
||
| 184 | } else { |
||
| 185 | return false; |
||
| 186 | } |
||
| 187 | } |
||
| 188 | 1 | return $pattern; |
|
| 189 | } |
||
| 190 | |||
| 191 | /** |
||
| 192 | * Extract an array of tables in a pattern |
||
| 193 | * |
||
| 194 | * @param string $pattern |
||
| 195 | * @return array |
||
| 196 | */ |
||
| 197 | 1 | private function getTablesFromPattern($pattern) |
|
| 198 | { |
||
| 199 | 1 | preg_match_all('/[A-Za-z_]+(?=\.[A-Za-z_]+ )/', $pattern, $tables); |
|
| 200 | 1 | if (is_null($tables)) { |
|
| 201 | return []; |
||
| 202 | } |
||
| 203 | 1 | return array_keys(array_flip($tables[0])); // unique tables only |
|
| 204 | } |
||
| 205 | |||
| 206 | /** |
||
| 207 | * Convert a v1 device group pattern to sql that can be ingested by jQuery-QueryBuilder |
||
| 208 | * |
||
| 209 | * @param $pattern |
||
| 210 | * @return array |
||
|
0 ignored issues
–
show
|
|||
| 211 | */ |
||
| 212 | 1 | private function convertV1Pattern($pattern) |
|
| 213 | { |
||
| 214 | 1 | $pattern = rtrim($pattern, ' &&'); |
|
| 215 | 1 | $pattern = rtrim($pattern, ' ||'); |
|
| 216 | |||
| 217 | 1 | $ops = ['=', '!=', '<', '<=', '>', '>=']; |
|
| 218 | 1 | $parts = str_getcsv($pattern, ' '); // tokenize the pattern, respecting quoted parts |
|
| 219 | 1 | $out = ""; |
|
| 220 | |||
| 221 | 1 | $count = count($parts); |
|
| 222 | 1 | for ($i = 0; $i < $count; $i++) { |
|
| 223 | 1 | $cur = $parts[$i]; |
|
| 224 | |||
| 225 | 1 | if (starts_with($cur, '%')) { |
|
| 226 | // table and column or macro |
||
| 227 | 1 | $out .= substr($cur, 1).' '; |
|
| 228 | 1 | } elseif (substr($cur, -1) == '~') { |
|
| 229 | // like operator |
||
| 230 | 1 | $content = $parts[++$i]; // grab the content so we can format it |
|
| 231 | |||
| 232 | 1 | if (starts_with($cur, '!')) { |
|
| 233 | // prepend NOT |
||
| 234 | 1 | $out .= 'NOT '; |
|
| 235 | } |
||
| 236 | |||
| 237 | 1 | $out .= "LIKE('".$this->convertRegexToLike($content)."') "; |
|
| 238 | 1 | } elseif ($cur == '&&') { |
|
| 239 | 1 | $out .= 'AND '; |
|
| 240 | 1 | } elseif ($cur == '||') { |
|
| 241 | 1 | $out .= 'OR '; |
|
| 242 | 1 | } elseif (in_array($cur, $ops)) { |
|
| 243 | // pass-through operators |
||
| 244 | 1 | $out .= $cur.' '; |
|
| 245 | } else { |
||
| 246 | // user supplied input |
||
| 247 | 1 | $out .= "'".trim($cur, '"\'')."' "; // TODO: remove trim, only needed with invalid input |
|
| 248 | } |
||
| 249 | } |
||
| 250 | 1 | return rtrim($out); |
|
| 251 | } |
||
| 252 | |||
| 253 | /** |
||
| 254 | * Convert sql regex to like, many common uses can be converted |
||
| 255 | * Should only be used to convert v1 patterns |
||
| 256 | * |
||
| 257 | * @param $pattern |
||
| 258 | * @return string |
||
| 259 | */ |
||
| 260 | 1 | private function convertRegexToLike($pattern) |
|
| 261 | { |
||
| 262 | 1 | $startAnchor = starts_with($pattern, '^'); |
|
| 263 | 1 | $endAnchor = ends_with($pattern, '$'); |
|
| 264 | |||
| 265 | 1 | $pattern = trim($pattern, '^$'); |
|
| 266 | |||
| 267 | 1 | $wildcards = ['@', '.*']; |
|
| 268 | 1 | if (str_contains($pattern, $wildcards)) { |
|
| 269 | // contains wildcard |
||
| 270 | 1 | $pattern = str_replace($wildcards, '%', $pattern); |
|
| 271 | } |
||
| 272 | |||
| 273 | // add ends appropriately |
||
| 274 | 1 | if ($startAnchor && !$endAnchor) { |
|
| 275 | $pattern .= '%'; |
||
| 276 | 1 | } elseif (!$startAnchor && $endAnchor) { |
|
| 277 | $pattern = '%'.$pattern; |
||
| 278 | } |
||
| 279 | |||
| 280 | // if there are no wildcards, assume substring |
||
| 281 | 1 | if (!str_contains($pattern, '%')) { |
|
| 282 | 1 | $pattern = '%'.$pattern.'%'; |
|
| 283 | } |
||
| 284 | |||
| 285 | 1 | return $pattern; |
|
| 286 | } |
||
| 287 | |||
| 288 | // ---- Accessors/Mutators ---- |
||
| 289 | |||
| 290 | /** |
||
| 291 | * Returns an sql formatted string |
||
| 292 | * Mostly, this is for ingestion by JQuery-QueryBuilder |
||
| 293 | * |
||
| 294 | * @return string |
||
| 295 | */ |
||
| 296 | public function getPatternSqlAttribute() |
||
| 297 | { |
||
| 298 | $sql = $this->pattern; |
||
| 299 | |||
| 300 | // fill in parameters |
||
| 301 | foreach ((array)$this->params as $value) { |
||
| 302 | if (!is_numeric($value) && !starts_with($value, "'")) { |
||
| 303 | $value = "'".$value."'"; |
||
| 304 | } |
||
| 305 | $sql = preg_replace('/\?/', $value, $sql, 1); |
||
| 306 | } |
||
| 307 | return $sql; |
||
| 308 | } |
||
| 309 | |||
| 310 | /** |
||
| 311 | * Fetch the device counts for groups |
||
| 312 | * Use DeviceGroups::with('deviceCountRelation') to eager load |
||
| 313 | * |
||
| 314 | * @return int |
||
| 315 | */ |
||
| 316 | public function getDeviceCountAttribute() |
||
| 317 | { |
||
| 318 | // if relation is not loaded already, let's do it first |
||
| 319 | if (!$this->relationLoaded('deviceCountRelation')) { |
||
| 320 | $this->load('deviceCountRelation'); |
||
| 321 | } |
||
| 322 | |||
| 323 | $related = $this->getRelation('deviceCountRelation')->first(); |
||
| 324 | |||
| 325 | // then return the count directly |
||
| 326 | return ($related) ? (int)$related->count : 0; |
||
| 327 | } |
||
| 328 | |||
| 329 | /** |
||
| 330 | * Custom mutator for params attribute |
||
| 331 | * Allows already encoded json to pass through |
||
| 332 | * |
||
| 333 | * @param array|string $params |
||
| 334 | */ |
||
| 335 | 1 | public function setParamsAttribute($params) |
|
| 336 | { |
||
| 337 | 1 | if (!Util::isJson($params)) { |
|
|
0 ignored issues
–
show
It seems like
$params defined by parameter $params on line 335 can also be of type array; however, App\Util::isJson() does only seem to accept string, maybe add an additional type check?
This check looks at variables that have been passed in as parameters and are passed out again to other methods. If the outgoing method call has stricter type requirements than the method itself, an issue is raised. An additional type check may prevent trouble. Loading history...
|
|||
| 338 | 1 | $params = json_encode($params); |
|
| 339 | } |
||
| 340 | |||
| 341 | 1 | $this->attributes['params'] = $params; |
|
| 342 | 1 | } |
|
| 343 | |||
| 344 | /** |
||
| 345 | * Check if the stored pattern is v1 |
||
| 346 | * Convert it to v2 for display |
||
| 347 | * Currently, it will only be updated in the database if the user saves the rule in the ui |
||
| 348 | * |
||
| 349 | * @param $pattern |
||
| 350 | * @return string |
||
| 351 | */ |
||
| 352 | 2 | public function getPatternAttribute($pattern) |
|
| 353 | { |
||
| 354 | // If this is a v1 pattern, convert it to sql |
||
| 355 | 2 | if (starts_with($pattern, '%')) { |
|
| 356 | 1 | return $this->convertV1Pattern($pattern); |
|
| 357 | } |
||
| 358 | |||
| 359 | 1 | return $pattern; |
|
| 360 | } |
||
| 361 | |||
| 362 | |||
| 363 | // ---- Define Relationships ---- |
||
| 364 | |||
| 365 | /** |
||
| 366 | * Relationship allows us to eager load device counts |
||
| 367 | * DeviceGroups::with('deviceCountRelation') |
||
| 368 | * |
||
| 369 | * @return mixed |
||
| 370 | */ |
||
| 371 | public function deviceCountRelation() |
||
| 372 | { |
||
| 373 | // FIXME this query doesn't work in strict mode |
||
| 374 | return $this->devices()->selectRaw('`device_group_device`.`device_group_id`, count(*) as count')->groupBy('device_group_device.device_group_id'); |
||
| 375 | } |
||
| 376 | |||
| 377 | /** |
||
| 378 | * Relationship to App\Models\Device |
||
| 379 | * |
||
| 380 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany |
||
| 381 | */ |
||
| 382 | 1 | public function devices() |
|
| 383 | { |
||
| 384 | 1 | return $this->belongsToMany('App\Models\Device', 'device_group_device', 'device_group_id', 'device_id'); |
|
| 385 | } |
||
| 386 | } |
||
| 387 |
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.