├── storage ├── logs │ └── .gitignore └── framework │ └── views │ └── .gitignore ├── _config.yml ├── public ├── assets │ ├── locales │ │ ├── es.json │ │ └── dataTables │ │ │ ├── nl.json │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ ├── es.json │ │ │ ├── ru.json │ │ │ └── fr.json │ ├── client_installer │ │ ├── payload │ │ │ ├── usr │ │ │ │ └── local │ │ │ │ │ ├── munkireport │ │ │ │ │ ├── scripts │ │ │ │ │ │ └── cache │ │ │ │ │ │ │ └── .gitignore │ │ │ │ │ └── munkilib │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ └── constants.py │ │ │ │ │ └── munki │ │ │ │ │ ├── postflight │ │ │ │ │ └── report_broken_client │ │ │ └── Library │ │ │ │ └── LaunchDaemons │ │ │ │ └── com.github.munkireport.runner.plist │ │ ├── build │ │ │ └── .gitignore │ │ └── build-info.plist │ ├── config │ │ └── settings.json │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── images │ │ ├── sort_asc.png │ │ ├── sort_both.png │ │ ├── sort_desc.png │ │ ├── favicons │ │ │ ├── favicon.ico │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── mstile-150x150.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── README.MD │ │ │ ├── browserconfig.xml │ │ │ └── manifest.json │ │ ├── sort_asc_disabled.png │ │ └── sort_desc_disabled.png │ ├── themes │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ ├── Default │ │ │ └── nvd3.override.css │ │ ├── Cosmo │ │ │ └── nvd3.override.css │ │ ├── Cyborg │ │ │ └── nvd3.override.css │ │ ├── Darkly │ │ │ └── nvd3.override.css │ │ ├── Flatly │ │ │ └── nvd3.override.css │ │ ├── Journal │ │ │ └── nvd3.override.css │ │ ├── Lumen │ │ │ └── nvd3.override.css │ │ ├── Paper │ │ │ └── nvd3.override.css │ │ ├── Simplex │ │ │ └── nvd3.override.css │ │ ├── Slate │ │ │ └── nvd3.override.css │ │ ├── Solar │ │ │ └── nvd3.override.css │ │ ├── United │ │ │ └── nvd3.override.css │ │ ├── Yeti │ │ │ └── nvd3.override.css │ │ ├── Cerulean │ │ │ └── nvd3.override.css │ │ ├── Readable │ │ │ └── nvd3.override.css │ │ ├── Sandstone │ │ │ └── nvd3.override.css │ │ ├── Spacelab │ │ │ └── nvd3.override.css │ │ └── Superhero │ │ │ └── nvd3.override.css │ ├── js │ │ ├── munkireport.autoupdate.js │ │ └── d3 │ │ │ └── LICENSE │ ├── html │ │ └── fatal_error.html │ └── css │ │ ├── system │ │ └── database.css │ │ ├── dataTables-bootstrap.css │ │ ├── admin.css │ │ └── bootstrap-markdown.min.css ├── .htaccess ├── index.php └── web.config ├── app ├── config │ ├── auth │ │ ├── noauth.php │ │ ├── env.php │ │ ├── local.php │ │ ├── ad.php │ │ ├── network.php │ │ ├── ldap.php │ │ └── saml.php │ ├── widget.php │ ├── dashboard.php │ ├── auth.php │ └── db.php ├── db │ └── README ├── helpers │ ├── test_helper.php │ ├── debug_helper.php │ ├── env_helper.php │ └── config_helper.php ├── views │ ├── widgets │ │ ├── spacer_widget.php │ │ ├── error_widget.php │ │ ├── unknown_widget.php │ │ ├── bargraph_widget.php │ │ ├── scrollbox_widget.php │ │ └── button_widget.php │ ├── auth │ │ ├── user.php │ │ ├── unauthorized.php │ │ ├── logout.php │ │ ├── unavailable.php │ │ ├── create_local_user.php │ │ └── login.php │ ├── json.php │ ├── install │ │ ├── modules_autopkg.php │ │ └── install_plist.php │ ├── partials │ │ └── alerts.php │ ├── client │ │ ├── summary_tab.php │ │ └── client_dont_exist.php │ ├── dashboard │ │ ├── dashboard.php │ │ └── business_unit.php │ ├── detail_widgets │ │ └── table_widget.php │ ├── error │ │ └── client_error.php │ └── system │ │ └── widget_gallery.php ├── models │ ├── Business_unit.php │ ├── Cache.php │ ├── Hash.php │ ├── MRModel.php │ └── Machine_group.php ├── controllers │ ├── Error.php │ ├── Settings.php │ ├── Archiver.php │ ├── Datatables.php │ ├── Show.php │ ├── Locale.php │ ├── Filter.php │ ├── Clients.php │ ├── Unit.php │ └── Module.php ├── Console │ └── Commands │ │ ├── FakerDataStore.php │ │ ├── StubTrait.php │ │ ├── UpCommand.php │ │ ├── DownCommand.php │ │ ├── MigrateCommand.php │ │ └── SeedCommand.php ├── lib │ └── munkireport │ │ ├── Themes.php │ │ ├── AuthNoauth.php │ │ ├── AuthEnv.php │ │ ├── Factory.php │ │ ├── User.php │ │ ├── Recaptcha.php │ │ ├── AuthWhitelist.php │ │ ├── Listing.php │ │ ├── AuthLDAP.php │ │ ├── Email.php │ │ ├── I18next.php │ │ ├── ArrayToPlist.php │ │ ├── AbstractAuth.php │ │ ├── LegacyMigrationSupport.php │ │ ├── Request.php │ │ ├── AuthLocal.php │ │ ├── Dashboard.php │ │ └── Unserializer.php └── processors │ └── Processor.php ├── .dockerignore ├── local ├── views │ └── widgets │ │ └── README.md ├── module_configs │ └── README.md ├── modules │ └── README.md ├── certs │ └── README.md ├── users │ └── README.md └── dashboards │ └── README.md ├── .gitattributes ├── tests ├── TestCase.php └── fixtures │ └── env.fixture ├── .gitignore ├── index.html ├── .pre-commit-config.yaml ├── database ├── migrations │ ├── 2020_05_09_194246_cache_init.php │ ├── 2017_02_20_085316_machine_group.php │ ├── 0213_00_00_000001_cleanup_hash_table.php │ ├── 2017_12_18_173230_business_unit.php │ └── 2017_02_12_235252_hash.php └── builders │ └── MRQueryBuilder.php ├── phpunit.xml ├── .travis.yml ├── docs ├── README.md ├── machine_groups.md ├── localize.md ├── autopkg.md ├── hacking.md ├── configure.md └── authorization.md ├── LICENSE.md ├── .github └── workflows │ └── github-registry.yml ├── please ├── Dockerfile ├── docker-compose.yml.example └── README.md /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /public/assets/locales/es.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | *.php 2 | -------------------------------------------------------------------------------- /app/config/auth/noauth.php: -------------------------------------------------------------------------------- 1 | 'REMOTE_USER', 5 | ]; 6 | -------------------------------------------------------------------------------- /public/assets/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/munkireport/munkireport-php/HEAD/public/assets/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /public/assets/images/sort_asc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/munkireport/munkireport-php/HEAD/public/assets/images/sort_asc.png -------------------------------------------------------------------------------- /public/assets/images/sort_both.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/munkireport/munkireport-php/HEAD/public/assets/images/sort_both.png -------------------------------------------------------------------------------- /public/assets/images/sort_desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/munkireport/munkireport-php/HEAD/public/assets/images/sort_desc.png -------------------------------------------------------------------------------- /app/config/auth/local.php: -------------------------------------------------------------------------------- 1 | env('AUTH_LOCAL_SEARCH_PATHS', [local_conf('users')]), 5 | ]; 6 | -------------------------------------------------------------------------------- /app/config/widget.php: -------------------------------------------------------------------------------- 1 | env('WIDGET_SEARCH_PATHS', [local_conf('views/widgets')]), 5 | ]; 6 | -------------------------------------------------------------------------------- /public/assets/client_installer/build/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all files in this dir... 2 | * 3 | 4 | # ... except for this one. 5 | !.gitignore 6 | -------------------------------------------------------------------------------- /app/helpers/test_helper.php: -------------------------------------------------------------------------------- 1 | "> 8 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | RemoveHandler cgi-script .pl .py .cgi 2 | 3 | RewriteEngine On 4 | 5 | RewriteCond %{REQUEST_FILENAME} !-f 6 | RewriteRule ^(.*)$ index.php?/$1 [L] 7 | -------------------------------------------------------------------------------- /public/assets/images/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/munkireport/munkireport-php/HEAD/public/assets/images/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/assets/images/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/munkireport/munkireport-php/HEAD/public/assets/images/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/assets/themes/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/munkireport/munkireport-php/HEAD/public/assets/themes/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/assets/themes/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/munkireport/munkireport-php/HEAD/public/assets/themes/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/assets/themes/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/munkireport/munkireport-php/HEAD/public/assets/themes/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/assets/images/favicons/README.MD: -------------------------------------------------------------------------------- 1 | To Generate Different Icons, use this website. 2 | http://realfavicongenerator.net/ 3 | 4 | Feel free to use the following files as templates. 5 | -------------------------------------------------------------------------------- /public/assets/themes/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/munkireport/munkireport-php/HEAD/public/assets/themes/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /app/views/auth/user.php: -------------------------------------------------------------------------------- 1 | 2 | modules 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/views/partials/alerts.php: -------------------------------------------------------------------------------- 1 | $list):?> 2 | 3 |

4 | 5 |

6 | 7 | 8 | -------------------------------------------------------------------------------- /public/assets/images/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #5d5858 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/assets/js/munkireport.autoupdate.js: -------------------------------------------------------------------------------- 1 | // Automatically refresh widgets 2 | $(document).on('appReady', function(e, lang) { 3 | 4 | var delay = 60; // seconds 5 | var refresh = function(){ 6 | 7 | $(document).trigger('appUpdate'); 8 | 9 | setTimeout(refresh, delay * 1000); 10 | } 11 | 12 | refresh(); 13 | 14 | }); -------------------------------------------------------------------------------- /local/certs/README.md: -------------------------------------------------------------------------------- 1 | # Certificates 2 | 3 | Use this folder to store certificates for SAML authentication 4 | 5 | These certs are used: 6 | 7 | `sp.crt` - the certificate of the service provider. 8 | `sp.key` - the private key of the service provider. 9 | `idp.crt` - the certificate to validate the response from the identity provider. 10 | 11 | -------------------------------------------------------------------------------- /app/models/Business_unit.php: -------------------------------------------------------------------------------- 1 | '; 8 | print_r($array); 9 | echo ';'; 10 | if ($halt) { 11 | exit; 12 | } 13 | } 14 | 15 | //-------------------------------------------------------------------------------- 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | app/db/* 2 | 3 | *.bak 4 | 5 | app/views/dashboard/custom_dashboard.php 6 | 7 | custom 8 | 9 | config.php 10 | *.DS_Store 11 | 12 | *.zip 13 | 14 | vendor/* 15 | 16 | composer.lock 17 | .env 18 | composer.local.* 19 | 20 | local/users/*.yml 21 | local/dashboards/*.yml 22 | local/certs/*.crt 23 | local/certs/*.key 24 | local/module_configs/*.yml 25 | local/modules/* 26 | 27 | .php_cs.cache 28 | composer 29 | storage/framework/down 30 | -------------------------------------------------------------------------------- /app/views/client/summary_tab.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | $data):?> 4 | 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | viewDetailWidget($data);?> 13 | 14 | 15 |
16 | -------------------------------------------------------------------------------- /app/controllers/Error.php: -------------------------------------------------------------------------------- 1 | $status_code); 20 | 21 | view('error/client_error', $data); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Console/Commands/FakerDataStore.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 |

8 | 9 |
10 | 11 |
12 | 13 | 16 | 17 |
18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /app/Console/Commands/StubTrait.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MunkiReport Security warning 6 | 7 | 8 |

MunkiReport Security warning

9 |

Please do not serve the project root, this makes your setup vulnerable

10 |

Only serve the public directory

11 |

See: MunkiReport Server setup: Source files

12 | 13 | -------------------------------------------------------------------------------- /app/config/dashboard.php: -------------------------------------------------------------------------------- 1 | env('DASHBOARD_SEARCH_PATHS', [local_conf('dashboards')]), 5 | 'template' => env('DASHBOARD_TEMPLATE', 'dashboard/dashboard'), 6 | 'default_layout' => [ 7 | [ 8 | 'client' => [], 9 | 'messages' => [], 10 | ], 11 | [ 12 | 'new_clients' => [], 13 | 'pending_apple' => [], 14 | 'pending_munki' => [], 15 | ], 16 | [ 17 | 'munki' => [], 18 | 'disk_report' => [], 19 | 'uptime' => [], 20 | ], 21 | ], 22 | ]; 23 | -------------------------------------------------------------------------------- /app/views/client/client_dont_exist.php: -------------------------------------------------------------------------------- 1 | view('partials/head'); ?> 2 |
3 |
4 |
5 |
6 |
7 |

8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 16 | view('partials/foot'); ?> 17 | -------------------------------------------------------------------------------- /app/views/widgets/unknown_widget.php: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |

8 | 9 |
10 | 11 |
12 | 13 | 16 | 17 | 18 |
19 | 20 |
21 | 22 |
23 | -------------------------------------------------------------------------------- /app/lib/munkireport/Themes.php: -------------------------------------------------------------------------------- 1 | themes[] = $theme; 23 | } 24 | } 25 | } 26 | 27 | public function get_list() 28 | { 29 | return $this->themes; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /public/assets/client_installer/payload/Library/LaunchDaemons/com.github.munkireport.runner.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.github.munkireport.runner 7 | Program 8 | /usr/local/munkireport/munkireport-runner 9 | RunAtLoad 10 | 11 | KeepAlive 12 | 13 | PathState 14 | 15 | /Users/Shared/.com.github.munkireport.run 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/assets/locales/dataTables/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "sLengthMenu": "_MENU_ resultaten weergeven", 3 | "sZeroRecords": "Geen resultaten gevonden", 4 | "sInfo": "_START_ tot _END_ van _TOTAL_ resultaten", 5 | "sInfoEmpty": "Geen resultaten om weer te geven", 6 | "sInfoFiltered": " (gefilterd uit _MAX_ resultaten)", 7 | "sInfoPostFix": "", 8 | "sSearch": "Zoeken", 9 | "sEmptyTable": "Geen resultaten aanwezig in de tabel", 10 | "sInfoThousands": ".", 11 | "sLoadingRecords": "Een moment geduld aub - bezig met laden...", 12 | "oPaginate": { 13 | "sFirst": "Eerste", 14 | "sLast": "Laatste", 15 | "sNext": "Volgende", 16 | "sPrevious": "Vorige" 17 | } 18 | } -------------------------------------------------------------------------------- /public/assets/client_installer/build-info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | distribution_style 6 | 7 | identifier 8 | com.github.munkireport 9 | install_location 10 | / 11 | name 12 | munkireport.pkg 13 | ownership 14 | recommended 15 | postinstall_action 16 | none 17 | suppress_bundle_relocation 18 | 19 | version 20 | 1.0 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/lib/munkireport/AuthNoauth.php: -------------------------------------------------------------------------------- 1 | config = $config; 12 | } 13 | 14 | public function login($login, $password) 15 | { 16 | return true; 17 | } 18 | 19 | public function getAuthMechanism() 20 | { 21 | return 'noauth'; 22 | } 23 | 24 | public function getAuthStatus() 25 | { 26 | return 'success'; 27 | } 28 | 29 | public function getUser() 30 | { 31 | return 'admin'; 32 | } 33 | 34 | public function getGroups() 35 | { 36 | return []; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/lib/munkireport/AuthEnv.php: -------------------------------------------------------------------------------- 1 | config = $config; 12 | } 13 | 14 | public function login($login, $password) 15 | { 16 | return true; 17 | } 18 | 19 | public function getAuthMechanism() 20 | { 21 | return 'env'; 22 | } 23 | 24 | public function getAuthStatus() 25 | { 26 | return 'success'; 27 | } 28 | 29 | public function getUser() 30 | { 31 | return getenv($this->config['env_user_var']); 32 | } 33 | 34 | public function getGroups() 35 | { 36 | return []; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/models/MRModel.php: -------------------------------------------------------------------------------- 1 | getConnection(); 13 | 14 | return new Builder( 15 | $connection, $connection->getQueryGrammar(), 16 | $connection->getPostProcessor() 17 | ); 18 | } 19 | 20 | public function toLabelCount() 21 | { 22 | $out = []; 23 | foreach($this->toArray() as $label => $value){ 24 | $out[] = ['label' => $label, 'count' => $value]; 25 | } 26 | return $out; 27 | } 28 | 29 | public $timestamps = false; 30 | 31 | } -------------------------------------------------------------------------------- /app/views/auth/unauthorized.php: -------------------------------------------------------------------------------- 1 | view('partials/head'); ?> 2 | 3 |
4 |
5 |
6 |
7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |

16 |

17 | 18 |
19 |
20 |
21 |
22 |
23 |
24 | 25 | view('partials/foot'); ?> 26 | -------------------------------------------------------------------------------- /app/views/dashboard/dashboard.php: -------------------------------------------------------------------------------- 1 | view('partials/head'); ?> 2 | 3 |
4 | 5 | 6 | 7 |
8 | 9 | $data):?> 10 | 11 | 12 | 13 | view($this, $data['widget'], $data); ?> 14 | 15 | 16 | 17 | view($this, $item, $data); ?> 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | view('partials/foot'); ?> 32 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.2.3 4 | hooks: 5 | - id: check-added-large-files 6 | args: [--maxkb=100] 7 | - id: check-ast 8 | - id: check-byte-order-marker 9 | - id: check-case-conflict 10 | # - id: check-docstring-first 11 | # - id: check-executables-have-shebangs 12 | - id: check-json 13 | - id: check-merge-conflict 14 | - id: check-xml 15 | # - id: check-yaml 16 | # - id: end-of-file-fixer 17 | - id: mixed-line-ending 18 | # - id: trailing-whitespace 19 | # args: [--markdown-linebreak-ext=md] 20 | - repo: https://github.com/digitalpulp/pre-commit-php 21 | rev: 1.3.0 22 | hooks: 23 | - id: php-lint-all 24 | - repo: https://github.com/python/black 25 | rev: 19.3b0 26 | hooks: 27 | - id: black 28 | -------------------------------------------------------------------------------- /local/users/README.md: -------------------------------------------------------------------------------- 1 | # Local users 2 | 3 | Users are defined by a file called `username.yml` 4 | This file should contain the password hash called `password_hash` 5 | You can generate these files by visiting `index.php?/auth/create_local_user` 6 | Please make sure that in your .env "AUTH_METHODS" contains "LOCAL", otherwise it will not work. 7 | 8 | ## Example: 9 | 10 | `geronimo.yml` 11 | 12 | contains 13 | 14 | ```yaml 15 | password_hash: $P$BnYH6NRCRO1lWHK6rkjFb0s.CmDtmm0 16 | ``` 17 | 18 | After you downloaded the `.yml` file you can drop it in the `users` directory in the root of the project and you should be able to log in. 19 | 20 | Note: You can provide alternative directory locations to store the users in by setting 21 | 22 | ```bash 23 | AUTH_LOCAL_SEARCH_PATHS=/path/to/your/users[, /another/path] 24 | ``` 25 | -------------------------------------------------------------------------------- /app/lib/munkireport/Factory.php: -------------------------------------------------------------------------------- 1 | files()->name('*.php')->in($path) as $file) { 24 | require $file->getRealPath(); 25 | } 26 | } elseif (is_file($path)) { 27 | require $path; 28 | } 29 | 30 | return $factory; 31 | } 32 | } -------------------------------------------------------------------------------- /local/dashboards/README.md: -------------------------------------------------------------------------------- 1 | # Dashboard layouts 2 | 3 | Put dashboard layout `YAML` files in this directory. Make sure the files end with `.yml`. 4 | To override the default dashboard, create a file called `default.yml`. 5 | 6 | Structure each file as follows: 7 | 8 | ```yaml 9 | display_name: My Awesome Dashboard 10 | hotkey: q 11 | row1: 12 | client: 13 | messages: 14 | row2: 15 | new_clients: 16 | pending_apple: 17 | pending_munki: 18 | row3: 19 | munki: 20 | disk_report: 21 | uptime: 22 | ``` 23 | 24 | You can use a widget multiple times on a row by explicitly stating the associated widget. 25 | This will be useful for widgets that accept additional data. 26 | 27 | ```yaml 28 | row1: 29 | uptime1: { widget: uptime } 30 | uptime2: { widget: uptime } 31 | uptime3: { widget: uptime } 32 | ``` 33 | 34 | -------------------------------------------------------------------------------- /public/assets/html/fatal_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Munkireport | Fatal error 7 | 8 | 9 |
10 |
11 |
12 |
13 |
14 |

Fatal error

15 |
16 |
17 | 18 |

Try again

19 |
20 |
21 |
22 |
23 |
24 | 25 | -------------------------------------------------------------------------------- /public/assets/locales/dataTables/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "sEmptyTable": "Keine Daten in der Tabelle vorhanden", 3 | "sInfo": "_START_ bis _END_ von _TOTAL_ Einträgen", 4 | "sInfoEmpty": "0 bis 0 von 0 Einträgen", 5 | "sInfoFiltered": "(gefiltert von _MAX_ Einträgen)", 6 | "sInfoPostFix": "", 7 | "sInfoThousands": ".", 8 | "sLengthMenu": "_MENU_ Einträge anzeigen", 9 | "sLoadingRecords": "Wird geladen...", 10 | "sSearch": "Suchen", 11 | "sZeroRecords": "Keine Einträge vorhanden.", 12 | "oPaginate": { 13 | "sFirst": "Erste", 14 | "sPrevious": "Zurück", 15 | "sNext": "Nächste", 16 | "sLast": "Letzte" 17 | }, 18 | "oAria": { 19 | "sSortAscending": ": aktivieren, um Spalte aufsteigend zu sortieren", 20 | "sSortDescending": ": aktivieren, um Spalte absteigend zu sortieren" 21 | } 22 | } -------------------------------------------------------------------------------- /app/config/auth.php: -------------------------------------------------------------------------------- 1 | env('RECAPTCHA_LOGIN_PUBLIC_KEY', ''), 14 | 'recaptchaloginprivatekey' => env('RECAPTCHA_LOGIN_PRIVATE_KEY', ''), 15 | 16 | /* 17 | |=============================================== 18 | | Force secure connection when authenticating 19 | |=============================================== 20 | | 21 | | Set this value to TRUE to force https when logging in. 22 | | This is useful for sites that serve MR both via http and https 23 | | 24 | */ 25 | 'auth_secure' => env('AUTH_SECURE', false), 26 | 27 | ]; -------------------------------------------------------------------------------- /public/assets/locales/dataTables/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "sEmptyTable": "No data available in table", 3 | "sInfo": "Showing _START_ to _END_ of _TOTAL_ entries", 4 | "sInfoEmpty": "Showing 0 to 0 of 0 entries", 5 | "sInfoFiltered": "(filtered from _MAX_ total entries)", 6 | "sInfoPostFix": "", 7 | "sInfoThousands": ",", 8 | "sLengthMenu": "_MENU_ records per page", 9 | "sLoadingRecords": "Loading...", 10 | "sSearch": "Search", 11 | "sZeroRecords": "No matching records found", 12 | "oPaginate": { 13 | "sFirst": "First", 14 | "sLast": "Last", 15 | "sNext": "Next", 16 | "sPrevious": "Previous" 17 | }, 18 | "oAria": { 19 | "sSortAscending": ": activate to sort column ascending", 20 | "sSortDescending": ": activate to sort column descending" 21 | } 22 | } -------------------------------------------------------------------------------- /database/migrations/2020_05_09_194246_cache_init.php: -------------------------------------------------------------------------------- 1 | create('cache', function (Blueprint $table) { 12 | $table->increments('id'); 13 | $table->string('module'); 14 | $table->string('property'); 15 | $table->mediumText('value'); 16 | $table->bigInteger('timestamp'); 17 | 18 | $table->index('module'); 19 | $table->index('property'); 20 | }); 21 | } 22 | 23 | public function down() 24 | { 25 | $capsule = new Capsule(); 26 | $capsule::schema()->dropIfExists('cache'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /public/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /public/assets/locales/dataTables/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "sLengthMenu": "Mostrar _MENU_ registros", 3 | "sZeroRecords": "No se encontraron resultados", 4 | "sEmptyTable": "Ningún dato disponible en esta tabla", 5 | "sInfo": "Mostrando registros del _START_ al _END_ de un total de _TOTAL_ registros", 6 | "sInfoEmpty": "Mostrando registros del 0 al 0 de un total de 0 registros", 7 | "sInfoFiltered": "(filtrado de un total de _MAX_ registros)", 8 | "sInfoPostFix": "", 9 | "sSearch": "Buscar", 10 | "sUrl": "", 11 | "sInfoThousands": ",", 12 | "sLoadingRecords": "Cargando...", 13 | "oPaginate": { 14 | "sFirst": "Primero", 15 | "sLast": "Último", 16 | "sNext": "Siguiente", 17 | "sPrevious": "Anterior" 18 | }, 19 | "oAria": { 20 | "sSortAscending": ": Activar para ordenar la columna de manera ascendente", 21 | "sSortDescending": ": Activar para ordenar la columna de manera descendente" 22 | } 23 | } -------------------------------------------------------------------------------- /public/assets/locales/dataTables/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "sEmptyTable": "В таблице отсутствуют данные", 3 | "sInfo": "Отображение с _START_ по _END_ из _TOTAL_ записей", 4 | "sInfoEmpty": "Отображение от 0 до 0 из 0 записей", 5 | "sInfoFiltered": "(отфильтровано из _MAX_ записей)", 6 | "sInfoPostFix": "", 7 | "sInfoThousands": " ", 8 | "sLengthMenu": "Показать _MENU_ записей на странице", 9 | "sLoadingRecords": "Загрузка записей...", 10 | "sSearch": "Поиск", 11 | "sZeroRecords": "Подходящие записи не найдены.", 12 | "oPaginate": { 13 | "sFirst": "Первая", 14 | "sLast": "Последняя", 15 | "sNext": "Следуюущая", 16 | "sPrevious": "Предыдущая" 17 | }, 18 | "oAria": { 19 | "sSortAscending": ": активировать для сортировки столбца по возрастанию", 20 | "sSortDescending": ": активировать для сортировки столбца по убыванию" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/Feature 14 | 15 | 16 | 17 | ./tests/Unit 18 | 19 | 20 | 21 | 22 | ./app 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /public/assets/locales/dataTables/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "sEmptyTable": "Aucune donnée disponible dans le table", 3 | "sInfo": "Afficher _START_ à _END_ de _TOTAL_ enregistrements", 4 | "sInfoEmpty": "Afficher 0 à 0 de 0 enregistrements", 5 | "sInfoFiltered": "(Résultat filtré d'un total de _MAX_ enregistrements)", 6 | "sInfoPostFix": "", 7 | "sInfoThousands": ",", 8 | "sLengthMenu": "_MENU_ enregistrements par page", 9 | "sLoadingRecords": "Chargement...", 10 | "sSearch": "Rechercher", 11 | "sZeroRecords": "Aucun enregistrement correspondant trouvé", 12 | "oPaginate": { 13 | "sFirst": "Première", 14 | "sLast": "Dernière", 15 | "sNext": "Suivante", 16 | "sPrevious": "Précédente" 17 | }, 18 | "oAria": { 19 | "sSortAscending": ": activer pour faire un tri de la colonne ascendant", 20 | "sSortDescending": ": activer pour faire un tri de la colonne descendant" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /public/assets/css/system/database.css: -------------------------------------------------------------------------------- 1 | .table-console { 2 | color: darkgrey; 3 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 4 | font-size: 1.4rem; 5 | 6 | padding: 1rem; 7 | } 8 | 9 | .table-console > tbody { 10 | /*overflow-y: scroll;*/ 11 | display: none; 12 | } 13 | 14 | .table-console td { 15 | border: none; 16 | padding: 2px 0; 17 | } 18 | 19 | .log-level-error { 20 | color: red; 21 | } 22 | 23 | .log-level-info { 24 | color: darkgrey; 25 | } 26 | 27 | .disclosure { 28 | cursor: pointer; 29 | } 30 | 31 | .disclosure > .glyphicon { 32 | transition-property: transform; 33 | transition-duration: 100ms; 34 | } 35 | 36 | .disclosure-active > .glyphicon { 37 | transform: rotate(90deg); 38 | } 39 | 40 | .table-console.disclosure-active tbody { 41 | display: block; 42 | } 43 | 44 | .loading { 45 | opacity: 0.5; 46 | transition-property: opacity; 47 | transition-duration: 100ms; 48 | } 49 | 50 | .database-alert { 51 | margin-top: 1rem; 52 | } 53 | -------------------------------------------------------------------------------- /app/views/auth/logout.php: -------------------------------------------------------------------------------- 1 | view('partials/head'); ?> 2 | 3 |
4 |
5 |
6 |
7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | $list): ?> 16 | 17 | 18 | 19 |

20 | 21 | 22 | 23 | 24 | 25 |

26 |

27 | 28 |
29 |
30 |
31 |
32 |
33 |
34 | 35 | view('partials/foot'); ?> 36 | -------------------------------------------------------------------------------- /app/views/auth/unavailable.php: -------------------------------------------------------------------------------- 1 | view('partials/head'); ?> 2 | 3 |
4 |
5 |
6 |
7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | $list): ?> 16 | 17 | 18 | 19 |

20 | 21 | 22 | 23 | 24 | 25 |

26 |

27 | 28 |
29 |
30 |
31 |
32 |
33 |
34 | 35 | view('partials/foot'); ?> 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '7.4' 4 | mysql: 5 | database: travis_ci_munkireport_php_test 6 | username: root 7 | encoding: utf8 8 | cache: 9 | directories: 10 | - vendor 11 | install: 12 | - composer install --no-dev 13 | 14 | script: 15 | - tar cfz "${HOME}/munkireport-php-${TRAVIS_TAG}.tar.gz" * .env.example 16 | - zip -r "${HOME}/munkireport-php-${TRAVIS_TAG}.zip" * .env.example 17 | 18 | before_deploy: 19 | - git config --local user.name "munkireport-php" 20 | - git config --local user.email "munkireport-php@users.noreply.github.com" 21 | - git tag "$(date +'%Y%m%d%H%M%S')-$(git log --format=%h -1)" 22 | 23 | deploy: 24 | provider: releases 25 | api_key: 26 | secure: FdFNw8Qz1doHtJ6x+SrXW2P0jDWv3sS32V4Qq61rVrphF0MSskwq+WtUVkuk5+Q9ZDTRGImqHOzxpWnYe336cthB5hrzjHyt4hpzno1X7BDZfoaMydNDHzGcAvQ9oEzrHaXrI2Fj94pHW1oKB+Pw4DSDYnsa3OyxbTnNHvb8uhU= 27 | file: 28 | - ${HOME}/munkireport-php-${TRAVIS_TAG}.zip 29 | - ${HOME}/munkireport-php-${TRAVIS_TAG}.tar.gz 30 | on: 31 | repo: munkireport/munkireport-php 32 | tags: true 33 | skip_cleanup: true 34 | -------------------------------------------------------------------------------- /app/processors/Processor.php: -------------------------------------------------------------------------------- 1 | module = $module; 15 | $this->serial_number = $serial_number; 16 | } 17 | 18 | /** 19 | * Store event 20 | * 21 | * Store event for this processor, assumes we have a serial_number 22 | * 23 | * @param string $type Use one of 'danger', 'warning', 'info' or 'success' 24 | * @param string $msg The message 25 | **/ 26 | public function store_event($type, $msg, $data = '') 27 | { 28 | store_event($this->serial_number, $this->module, $type, $msg, $data); 29 | } 30 | 31 | /** 32 | * Delete event 33 | * 34 | * Delete event for this model, assumes we have a serial_number 35 | * 36 | **/ 37 | public function delete_event() 38 | { 39 | delete_event($this->serial_number, $this->module); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | Documentation 2 | ============= 3 | 4 | The documentation of munkireport is a work in progresss, if you want to contribute, please fork the wiki and send a pull request. 5 | 6 | Installation 7 | --- 8 | 9 | The installation is covered in setup.md 10 | 11 | Performance 12 | --- 13 | 14 | Running munkireport with up to 400 (300 daily active) clients with the SQLite backend should be ok. If you're adding more clients you'll need a database backend that allows for more concurrency like MySQL. 15 | 16 | Security 17 | --- 18 | 19 | Although munkireport should be pretty secure out-of-the-box, there are some areas where you could improve the security. 20 | 21 | #### Use https for the webserver 22 | 23 | Logging into the munkireport webapp sends your username and password in cleartext, someone might intercept your credentials and use them to 24 | login. If you use https, the communication between your browser and the webserver is encrypted. 25 | 26 | #### Remove the database from the documentroot 27 | 28 | Add instructions. 29 | 30 | #### Remove the webapp from the documentroot 31 | 32 | Add instructions. 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 munkireport 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/config/auth/ad.php: -------------------------------------------------------------------------------- 1 | env('AUTH_AD_SCHEMA', 'ActiveDirectory'), 5 | 'account_prefix' => env('AUTH_AD_ACCOUNT_PREFIX', NULL), 6 | 'account_suffix' => env('AUTH_AD_ACCOUNT_SUFFIX', NULL), 7 | 'username' => env('AUTH_AD_USERNAME', NULL), 8 | 'password' => env('AUTH_AD_PASSWORD', NULL), 9 | 'base_dn' => env('AUTH_AD_BASE_DN', 'dc=mydomain,dc=local'), 10 | 'hosts' => env('AUTH_AD_HOSTS', []), 11 | 'port' => env('AUTH_AD_PORT', 389), 12 | 'follow_referrals' => env('AUTH_AD_FOLLOW_REFERRALS', false), 13 | 'use_ssl' => env('AUTH_AD_USE_SSL', false), 14 | 'use_tls' => env('AUTH_AD_USE_TLS', false), 15 | 'version' => env('AUTH_AD_VERSION', 3), 16 | 'timeout' => env('AUTH_AD_TIMEOUT', 5), 17 | 'mr_allowed_users' => env('AUTH_AD_ALLOWED_USERS', []), 18 | 'mr_allowed_groups' => env('AUTH_AD_ALLOWED_GROUPS', []), 19 | 'mr_recursive_groupsearch' => env('AUTH_AD_RECURSIVE_GROUPSEARCH', false), 20 | ]; 21 | -------------------------------------------------------------------------------- /app/helpers/env_helper.php: -------------------------------------------------------------------------------- 1 | env('AUTH_NETWORK_WHITELIST_IP4', []), 26 | 'redirect_unauthorized' => env('AUTH_NETWORK_REDIRECT_UNAUTHORIZED', ''), 27 | ]; 28 | -------------------------------------------------------------------------------- /.github/workflows/github-registry.yml: -------------------------------------------------------------------------------- 1 | name: Create and publish a Docker image 2 | 3 | on: 4 | push: 5 | branches: ['5.x'] 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | 11 | jobs: 12 | build-and-push-image: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v3 21 | 22 | - name: Log in to the Container registry 23 | uses: docker/login-action@v2 24 | with: 25 | registry: ${{ env.REGISTRY }} 26 | username: ${{ github.actor }} 27 | password: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | - name: Extract metadata (tags, labels) for Docker 30 | id: meta 31 | uses: docker/metadata-action@v4 32 | with: 33 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 34 | 35 | - name: Build and push Docker image 36 | uses: docker/build-push-action@v4 37 | with: 38 | context: . 39 | push: true 40 | tags: ${{ steps.meta.outputs.tags }} 41 | labels: ${{ steps.meta.outputs.labels }} 42 | -------------------------------------------------------------------------------- /app/Console/Commands/UpCommand.php: -------------------------------------------------------------------------------- 1 | comment('MunkiReport is already up.'); 34 | 35 | return true; 36 | } 37 | 38 | unlink(storage_path('framework/down')); 39 | 40 | $this->info('MunkiReport is now live.'); 41 | } catch (Exception $e) { 42 | $this->error('Failed to disable maintenance mode.'); 43 | 44 | $this->error($e->getMessage()); 45 | 46 | return 1; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/controllers/Settings.php: -------------------------------------------------------------------------------- 1 | authorized()) { 13 | view('json', array('msg' => 'Not authorized')); 14 | 15 | die(); 16 | } 17 | } 18 | 19 | //=============================================================== 20 | 21 | /** 22 | * Set 23 | * 24 | * Set/Get theme value in $_SESSION 25 | * 26 | */ 27 | public function theme() 28 | { 29 | if(isset($_POST['set'])) 30 | { 31 | // Check if valid theme 32 | $themeObj = new Themes(); 33 | if(in_array($_POST['set'], $themeObj->get_list())) 34 | { 35 | sess_set('theme', $_POST['set']); 36 | } 37 | else 38 | { 39 | view('json', array('msg' => sprintf('Error: theme %s unknown', $_POST['set']))); 40 | } 41 | } 42 | 43 | view('json', array('msg' => sess_get('theme', conf('default_theme')))); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /docs/machine_groups.md: -------------------------------------------------------------------------------- 1 | # About Machine Groups 2 | 3 | Machine Groups can be used to group machines together. Machines have to register themselves into the available machine groups. To do this, all machines have to have a key that corresponds to the machinegroup they belong to. 4 | 5 | ## Create a Machine Group 6 | 7 | To create a Machine Group, open the Munkireport webinterface and click on 'Admin->Manage Business Units'. If you don't have Business Units enabled, you'll see a panel with the title 'Unassigned Groups'. Click on the + sign to add a new group. Give the group a name and type a Machine Key or click on 'generate' to generate a random GUID-style key. 8 | 9 | ## Deploy Group keys 10 | 11 | To deploy the group key, you'll have to add it to the machines running munkireport. On the client, the group key is stored in the 'Passphrase' property in the MunkiReport preferences file. To manually set this key, you can type the following command: 12 | 13 | ```sh 14 | sudo defaults write /Library/Preferences/MunkiReport Passphrase 'FE0E7F5F-5396-CCE5-3821-52055981CC94' 15 | ``` 16 | 17 | For new machines, you could add this to a first-boot script/package. Depending on your setup, you could set this value with munki (anyone can retrieve this value from you munki repo which might not be desirable). 18 | -------------------------------------------------------------------------------- /please: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | setCatchExceptions(true); 31 | 32 | $app->add(new App\Console\Commands\MigrateCommand); 33 | $app->add(new App\Console\Commands\ModuleCommand); 34 | $app->add(new App\Console\Commands\MigrationCommand); 35 | $app->add(new App\Console\Commands\SeedCommand); 36 | $app->add(new App\Console\Commands\UpCommand); 37 | $app->add(new App\Console\Commands\DownCommand); 38 | 39 | $app->run(); -------------------------------------------------------------------------------- /app/lib/munkireport/User.php: -------------------------------------------------------------------------------- 1 | conf = $conf; 18 | $this->session = $_SESSION; 19 | } 20 | 21 | public function isAdmin() 22 | { 23 | return $this->_getRole() == 'admin'; 24 | } 25 | 26 | public function isManager() 27 | { 28 | return $this->_getRole() == 'manager'; 29 | } 30 | 31 | public function isArchiver() 32 | { 33 | return $this->_getRole() == 'archiver'; 34 | } 35 | 36 | public function canArchive() 37 | { 38 | return $this->isAdmin() || $this->isManager() || $this->isArchiver(); 39 | } 40 | 41 | public function canAccessMachineGroup($id) 42 | { 43 | if ($this->isAdmin()) { 44 | return true; 45 | } 46 | 47 | return in_array($id, $this->machineGroups()); 48 | } 49 | 50 | public function machineGroups() 51 | { 52 | return $this->session['machine_groups'] ?? []; 53 | } 54 | 55 | private function _getRole() 56 | { 57 | return $this->session['role'] ?? 'nobody'; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/config/auth/ldap.php: -------------------------------------------------------------------------------- 1 | env('AUTH_LDAP_SERVER', 'ldap.server.local'), 5 | 'usertree' => env('AUTH_LDAP_USER_BASE', 'uid=%{user},cn=users,dc=server,dc=local'), 6 | 'grouptree' => env('AUTH_LDAP_GROUP_BASE', 'cn=groups,dc=server,dc=local'), 7 | 'mr_allowed_users' => env('AUTH_LDAP_ALLOWED_USERS', []), 8 | 'mr_allowed_groups' => env('AUTH_LDAP_ALLOWED_GROUPS', []), 9 | 10 | 'userfilter' => env('AUTH_LDAP_USER_FILTER', '(&(uid=%{user})(objectClass=posixAccount))'), 11 | 'groupfilter' => env('AUTH_LDAP_GROUP_FILTER', '(&(objectClass=posixGroup)(memberUID=%{uid}))'), 12 | 'port' => env('AUTH_LDAP_PORT', 389), 13 | 'version' => env('AUTH_LDAP_VERSION', 3), 14 | 'starttls' => env('AUTH_LDAP_USE_STARTTLS', false), 15 | 'referrals' => env('AUTH_LDAP_FOLLOW_REFERRALS', false), 16 | 'deref' => env('AUTH_LDAP_DEREF', LDAP_DEREF_NEVER), 17 | 18 | 'binddn' => env('AUTH_LDAP_BIND_DN', ''), 19 | 'bindpw' => env('AUTH_LDAP_BIND_PASSWORD', ''), 20 | 'userscope' => env('AUTH_LDAP_USER_SCOPE', 'sub'), 21 | 'groupscope' => env('AUTH_LDAP_GROUP_SCOPE', 'sub'), 22 | 'groupkey' => env('AUTH_LDAP_GROUP_KEY', 'cn'), 23 | 'debug' => env('AUTH_LDAP_DEBUG', false), 24 | ]; 25 | 26 | -------------------------------------------------------------------------------- /app/views/detail_widgets/table_widget.php: -------------------------------------------------------------------------------- 1 |
4 | > 5 |

6 | " : ''?> 7 | 9 | 10 | > 11 |

12 | 14 | > 15 | 16 | 17 | 18 | 19 | 28 | 29 | 30 | 31 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
32 |
33 | 34 | 35 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.2-apache 2 | 3 | ENV APP_DIR /var/munkireport 4 | 5 | RUN apt-get update && \ 6 | apt-get install --no-install-recommends -y libldap2-dev \ 7 | libcurl4-openssl-dev \ 8 | libzip-dev \ 9 | unzip \ 10 | zlib1g-dev \ 11 | libxml2-dev && \ 12 | apt-get clean && \ 13 | rm -rf /var/lib/apt/lists/* 14 | 15 | RUN docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ && \ 16 | docker-php-ext-install -j$(nproc) curl pdo_mysql soap ldap zip 17 | 18 | ENV COMPOSER_ALLOW_SUPERUSER 1 19 | ENV COMPOSER_HOME /tmp 20 | ENV SITENAME MunkiReport 21 | ENV MODULES ard, bluetooth, disk_report, munkireport, managedinstalls, munkiinfo, network, security, warranty 22 | ENV INDEX_PAGE "" 23 | ENV AUTH_METHODS NOAUTH 24 | 25 | COPY . $APP_DIR 26 | 27 | WORKDIR $APP_DIR 28 | 29 | COPY --from=composer:2.2.6 /usr/bin/composer /usr/local/bin/composer 30 | 31 | RUN composer install --no-dev && \ 32 | composer dumpautoload -o 33 | 34 | RUN mkdir -p app/db && \ 35 | chmod -R 777 app/db 36 | 37 | RUN php please migrate 38 | 39 | RUN rm -rf /var/www/html && \ 40 | ln -s /var/munkireport/public /var/www/html 41 | 42 | RUN sed -i 's/ServerTokens OS/ServerTokens Prod/' /etc/apache2/conf-available/security.conf 43 | 44 | RUN sed -i 's/ServerSignature On/ServerSignature Off/' /etc/apache2/conf-available/security.conf 45 | 46 | RUN a2enmod rewrite 47 | 48 | EXPOSE 80 49 | -------------------------------------------------------------------------------- /public/assets/css/dataTables-bootstrap.css: -------------------------------------------------------------------------------- 1 | 2 | div.dataTables_length label { 3 | float: left; 4 | text-align: left; 5 | } 6 | 7 | div.dataTables_length select { 8 | width: 75px; 9 | } 10 | 11 | div.dataTables_filter label { 12 | float: right; 13 | } 14 | 15 | div.dataTables_info { 16 | padding-top: 8px; 17 | } 18 | 19 | div.dataTables_paginate { 20 | float: right; 21 | margin: 0; 22 | } 23 | 24 | table.table { 25 | clear: both; 26 | margin-bottom: 6px !important; 27 | } 28 | 29 | table.table thead .sorting, 30 | table.table thead .sorting_asc, 31 | table.table thead .sorting_desc, 32 | table.table thead .sorting_asc_disabled, 33 | table.table thead .sorting_desc_disabled { 34 | cursor: pointer; 35 | *cursor: hand; 36 | } 37 | 38 | table.table thead .sorting { background: none center right; } 39 | table.table thead .sorting { background: url('../images/sort_both.png') no-repeat center right; } 40 | table.table thead .sorting_asc { background: url('../images/sort_asc.png') no-repeat center right; } 41 | table.table thead .sorting_desc { background: url('../images/sort_desc.png') no-repeat center right; } 42 | 43 | table.table thead .sorting_asc_disabled { background: url('../images/sort_asc_disabled.png') no-repeat center right; } 44 | table.table thead .sorting_desc_disabled { background: url('../images/sort_desc_disabled.png') no-repeat center right; } 45 | table.dataTable th:active { 46 | outline: none; 47 | } -------------------------------------------------------------------------------- /public/assets/client_installer/payload/usr/local/munki/postflight: -------------------------------------------------------------------------------- 1 | #!/usr/local/munki/munki-python 2 | 3 | import os 4 | import subprocess 5 | import sys 6 | 7 | TOUCH_FILE_PATH = '/Users/Shared/.com.github.munkireport.run' 8 | LAUNCHD = 'com.github.munkireport.runner' 9 | LAUNCHD_PATH = '/Library/LaunchDaemons/{}.plist'.format(LAUNCHD) 10 | SUBMIT_SCRIPT = '/usr/local/munkireport/munkireport-runner' 11 | 12 | def write_touch_file(): 13 | if os.path.exists(TOUCH_FILE_PATH): 14 | os.remove(TOUCH_FILE_PATH) 15 | 16 | if not os.path.exists(TOUCH_FILE_PATH): 17 | with open(TOUCH_FILE_PATH, 'a'): 18 | os.utime(TOUCH_FILE_PATH, None) 19 | 20 | def ensure_launchd_loaded(): 21 | cmd =[ 22 | '/bin/launchctl', 23 | 'list' 24 | ] 25 | loaded_launchds = subprocess.check_output(cmd).decode('utf-8', 'ignore') 26 | # load the launchd if it's not loaded and is present on disk 27 | if LAUNCHD not in loaded_launchds and os.path.exists(LAUNCHD_PATH): 28 | cmd = [ 29 | '/bin/launchctl', 30 | 'load', 31 | LAUNCHD_PATH 32 | ] 33 | subprocess.check_call(cmd) 34 | 35 | def main(): 36 | write_touch_file() 37 | ensure_launchd_loaded() 38 | # If the launchd isn't present, call the submit script old school 39 | if not os.path.exists(LAUNCHD_PATH): 40 | subprocess.check_call(SUBMIT_SCRIPT) 41 | 42 | if __name__ == '__main__': 43 | main() 44 | -------------------------------------------------------------------------------- /app/config/db.php: -------------------------------------------------------------------------------- 1 | 'sqlite', 8 | 'database' => env('CONNECTION_DATABASE', APP_ROOT . 'app/db/db.sqlite'), 9 | 'username' => '', 10 | 'password' => '', 11 | 'options' => env('CONNECTION_OPTIONS', []), 12 | ]; 13 | break; 14 | case 'mysql': 15 | return [ 16 | 'driver' => 'mysql', 17 | 'host' => env('CONNECTION_HOST', '127.0.0.1'), 18 | 'port' => env('CONNECTION_PORT', 3306), 19 | 'database' => env('CONNECTION_DATABASE', 'munkireport'), 20 | 'username' => env('CONNECTION_USERNAME', 'munkireport'), 21 | 'password' => env('CONNECTION_PASSWORD', 'munkireport'), 22 | 'charset' => env('CONNECTION_CHARSET', 'utf8mb4'), 23 | 'collation' => env('CONNECTION_COLLATION', 'utf8mb4_unicode_ci'), 24 | 'strict' => env('CONNECTION_STRICT', true), 25 | 'engine' => env('CONNECTION_ENGINE', 'InnoDB'), 26 | 'ssl_enabled' => env('CONNECTION_SSL_ENABLED', false), 27 | 'ssl_key' => env('CONNECTION_SSL_KEY'), 28 | 'ssl_cert' => env('CONNECTION_SSL_CERT'), 29 | 'ssl_ca' => env('CONNECTION_SSL_CA'), 30 | 'ssl_capath' => env('CONNECTION_SSL_CAPATH'), 31 | 'ssl_cipher' => env('CONNECTION_SSL_CIPHER'), 32 | 'options' => env('CONNECTION_OPTIONS', []), 33 | ]; 34 | default: 35 | throw new \Exception(sprintf("Unknown driver: %s", $driver), 1); 36 | break; 37 | } 38 | -------------------------------------------------------------------------------- /docker-compose.yml.example: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | munkireport: 4 | image: ghcr.io/munkireport/munkireport-php:5.x 5 | restart: always 6 | environment: 7 | - MODULES=applications, directory_service, disk_report, displays_info, extensions, filevault_status, homebrew, homebrew_info, ibridge, installhistory, inventory, localadmin, managedinstalls, mdm_status, munkiinfo, munkireport, munkireportinfo, network, power, printer, profile, security, softwareupdate, sophos, supported_os, timemachine, usage_stats, user_sessions, warranty, wifi 8 | - SITENAME=Munkireport 9 | - CONNECTION_DRIVER=mysql 10 | - CONNECTION_HOST=db 11 | - CONNECTION_PORT=3306 12 | - CONNECTION_DATABASE=munkireport 13 | - CONNECTION_USERNAME=munkireport 14 | - CONNECTION_PASSWORD= 15 | - AUTH_METHODS=LOCAL 16 | - PUID=1000 17 | - PGID=1000 18 | - CLIENT_PASSPHRASES= 19 | - WEBHOST=https://munkireport.domain.com 20 | - TZ=Europe/Berlin 21 | depends_on: 22 | - db 23 | ports: 24 | - 80:80 25 | volumes: 26 | - ./munkireport-db/:/var/munkireport/app/db 27 | - ./user//:/var/munkireport/local/users 28 | db: 29 | image: mariadb:latest 30 | restart: always 31 | environment: 32 | - MYSQL_ROOT_PASSWORD= 33 | - MYSQL_DATABASE=munkireport 34 | - MYSQL_USER=munkireport 35 | - MYSQL_PASSWORD= 36 | - PUID=1000 37 | - PGID=1000 38 | - TZ=Europe/Berlin 39 | volumes: 40 | - ./db/:/var/lib/mysql 41 | -------------------------------------------------------------------------------- /public/assets/js/d3/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2015, Michael Bostock 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * The name Michael Bostock may not be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, 21 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 22 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 24 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 26 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /public/assets/css/admin.css: -------------------------------------------------------------------------------- 1 | .admin_grid{ 2 | margin-top: 1em; 3 | width: 400px; 4 | } 5 | .admin_grid img{ 6 | border: 0; 7 | vertical-align: middle; 8 | } 9 | 10 | a.add_link, a.edit_link{ 11 | background-repeat: no-repeat; 12 | padding-left: 20px; 13 | } 14 | a.add_link{ 15 | background-image: url('/images/add.png'); 16 | } 17 | a.edit_link{ 18 | background-image: url('/images/pencil.png'); 19 | } 20 | 21 | /* admin list view */ 22 | .crud_table .grid { 23 | border-collapse: collapse; 24 | border: 1px solid #ccc; 25 | } 26 | .crud_table .grid .odd{ 27 | background-color: #eee; 28 | } 29 | .crud_table .grid th{ 30 | background: url("/images/menubg.png") no-repeat scroll -20px top transparent; 31 | color: #fff; 32 | font-weight: normal; 33 | text-transform: capitalize; 34 | } 35 | .crud_table .grid td a{ 36 | color: #286571; 37 | } 38 | .crud_table .grid td.col_0 div{ 39 | float: left; 40 | } 41 | .crud_table .grid td.col_0 .edit_link, .crud_table .grid td.col_0 .delete-button{ 42 | display:block; 43 | height:17px; 44 | width:20px; 45 | padding:0; 46 | text-indent:-400px; 47 | background-repeat: no-repeat; 48 | } 49 | .crud_table .grid td.col_0 .delete-button{ 50 | font-size: inherit; 51 | background-image: url('/images/delete.png'); 52 | cursor: pointer; 53 | } 54 | 55 | /* admin add/edit view */ 56 | .crud_edit label, .crud_add label{ 57 | font-size: 12px; 58 | } 59 | .crud_edit input, .crud_add input, 60 | .crud_edit select, .crud_add select{ 61 | width: 15em; 62 | } -------------------------------------------------------------------------------- /docs/localize.md: -------------------------------------------------------------------------------- 1 | Localizing 2 | ========== 3 | 4 | If you are developing for MunkiReport, please make sure the strings you use are provided via the i18n localization framework. 5 | 6 | Files 7 | ----- 8 | 9 | The localization files are located in `assets/locales/`. These are JSON files. The JSON format has some restrictions in the use of quotations etc. so please make sure the locale files are valid JSON, you can check your file using a validator like [jsonlint.com](http://jsonlint.com). 10 | 11 | dataTables 12 | ---------- 13 | 14 | When you're adding an additional language, make sure you add the appropriate localization file for `assets/locales/dataTables` as well, the tables won't load if it can't find the locale file. You can find dataTables locale files in the [dataTables github repo](https://github.com/DataTables/Plugins/tree/master/i18n). 15 | Make sure you: 16 | 17 | * remove the comments at the beginning of the file 18 | * remove the sProcessing property (MunkiReport shows a spinner instead) 19 | * remove the colon (:) from sSearch (MunkiReport moves this into the placeholder) 20 | 21 | Some things to keep in mind 22 | --------------------------- 23 | 24 | * Place generic words like 'computer', 'memory', 'hour' in the root of the JSON object 25 | * Try to find an appropriate place for other words and sentences (look at what's already localized) 26 | * English is the fallback language, so make sure the strings are at least available in en.json 27 | * Try to keep the JSON files alphabetically organized, this will make it a lot easier for people maintaining localization files. -------------------------------------------------------------------------------- /app/views/error/client_error.php: -------------------------------------------------------------------------------- 1 | 2 | view('partials/head'); ?> 3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 |
13 | 14 |

Error

15 | 16 |
17 | 18 |
19 | 20 | 21 |

22 | 23 | 24 | 25 | 26 | You are not allowed to view this page 27 | 28 | 29 | 30 | Page not found 31 | 32 | 33 | 34 | You are required to visit this site using a secure connection. 35 | Go to secure site 36 | 37 | 38 | 39 | 40 | MunkiReport is down for maintenance. 41 | 42 | 43 | 44 | Unknown error 45 | 46 | 47 | 48 |

49 | 50 |

51 | 52 |

53 | 54 | 55 | 56 |
57 | 58 |
59 | 60 |
61 | 62 |
63 | 64 |
65 | 66 | view('partials/foot'); ?> 67 | -------------------------------------------------------------------------------- /public/assets/client_installer/payload/usr/local/munkireport/munkilib/constants.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # 3 | # Copyright 2009-2023 Greg Neagle. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the 'License'); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an 'AS IS' BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | """constants.py. 17 | 18 | Created by Greg Neagle on 2016-12-14. 19 | 20 | Commonly used constants 21 | """ 22 | 23 | # NOTE: it's very important that defined exit codes are never changed! 24 | # Preflight exit codes. 25 | EXIT_STATUS_PREFLIGHT_FAILURE = 1 # Python crash yields 1. 26 | # Client config exit codes. 27 | EXIT_STATUS_OBJC_MISSING = 100 28 | EXIT_STATUS_MUNKI_DIRS_FAILURE = 101 29 | # Server connection exit codes. 30 | EXIT_STATUS_SERVER_UNAVAILABLE = 150 31 | # User related exit codes. 32 | EXIT_STATUS_INVALID_PARAMETERS = 200 33 | EXIT_STATUS_ROOT_REQUIRED = 201 34 | 35 | BUNDLE_ID = "MunkiReport" 36 | MANAGED_INSTALLS_PLIST_PATH = "/Library/Preferences/" + BUNDLE_ID + ".plist" 37 | SECURE_MANAGED_INSTALLS_PLIST_PATH = ( 38 | "/private/var/root/Library/Preferences/" + BUNDLE_ID + ".plist" 39 | ) 40 | 41 | ADDITIONAL_HTTP_HEADERS_KEY = "AdditionalHttpHeaders" 42 | 43 | if __name__ == "__main__": 44 | print("This is a library of support tools for the Munki Suite.") 45 | -------------------------------------------------------------------------------- /app/controllers/Archiver.php: -------------------------------------------------------------------------------- 1 | authorized() || jsonError('Authenticate first', 403); 13 | $this->authorized('archive') || jsonError('You need to be archiver, manager or admin', 403); 14 | 15 | // Connect to database 16 | $this->connectDB(); 17 | } 18 | 19 | 20 | //=============================================================== 21 | 22 | public function index() 23 | { 24 | echo 'Archiver'; 25 | } 26 | 27 | //=============================================================== 28 | 29 | public function update_status($serial_number = '') 30 | { 31 | if (! isset($_POST['status'])) { 32 | jsonError('No status found'); 33 | } 34 | $changes = Reportdata_model::where('serial_number', $serial_number) 35 | ->update( 36 | [ 37 | 'archive_status' => intval($_POST['status']), 38 | ] 39 | ); 40 | jsonView(['updated' => intval($_POST['status'])]); 41 | } 42 | 43 | public function bulk_update_status() 44 | { 45 | if( ! $days = intval(post('days'))){ 46 | jsonError('No days sent'); 47 | } 48 | $expire_timestamp = time() - ($days * 24 * 60 * 60); 49 | $changes = Reportdata_model::where('timestamp', '<', $expire_timestamp) 50 | ->where('archive_status', 0) 51 | ->update(['archive_status' => 1]); 52 | jsonView(['updated' => $changes]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/lib/munkireport/Recaptcha.php: -------------------------------------------------------------------------------- 1 | secret = $secret; 22 | } 23 | 24 | public function verify($recaptcharesponse, $userip) 25 | { 26 | 27 | //verifying recaptcha with google 28 | try { 29 | $data = array( 30 | 'secret' => $this->secret, 31 | 'response' => $recaptcharesponse, 32 | 'remoteip' => $userip, 33 | ); 34 | $options = array( 35 | 'http' => array( 36 | 'header' => "Content-type: application/x-www-form-urlencoded\r\n", 37 | 'method' => 'POST', 38 | 'content' => http_build_query($data), 39 | ) 40 | ); 41 | $options = array( 42 | 'form_params' => [ 43 | 'secret' => $this->secret, 44 | 'response' => $recaptcharesponse, 45 | 'remoteip' => $userip, 46 | ], 47 | ); 48 | $client = new Request(); 49 | $result = $client->post($this->url, $options); 50 | $this->json_result = json_decode($result); 51 | 52 | if ($this->json_result->success == 1) { 53 | return true; 54 | } 55 | } catch (Exception $e) { 56 | error($e->getMessage(), ''); 57 | } 58 | 59 | return false; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/lib/munkireport/AuthWhitelist.php: -------------------------------------------------------------------------------- 1 | config = $config; 11 | } 12 | 13 | private function parse_range($ip, $range) { 14 | if (strpos($range, '/')) return $this->cidr_match($ip, $range); 15 | $range = $range . '/32'; 16 | return $this->cidr_match($ip, $range); 17 | } 18 | 19 | private function cidr_match($ip, $range) { 20 | list ($subnet, $bits) = explode('/', $range); 21 | $ip = ip2long($ip); 22 | $subnet = ip2long($subnet); 23 | $mask = -1 << (32 - $bits); 24 | $subnet &= $mask; # nb: in case the supplied subnet wasn't correctly aligned 25 | return ($ip & $mask) == $subnet; 26 | } 27 | 28 | public function check_ip($remote_address) { 29 | // if user is going to the report uri, allow the connection regardless 30 | if (substr(($GLOBALS[ 'engine' ]->get_uri_string()), 0, 8) === "report/") { return 1; } 31 | 32 | // for loop through the configuration setting to check if any IP addresses match - if so 33 | // allow traffic 34 | 35 | foreach ($this->config['whitelist_ipv4'] as $range) { 36 | if ($this->parse_range($remote_address, $range)) { return 1; } 37 | } 38 | 39 | // if a custom 403 page is defined, send traffic to that page 40 | if (isset($this->config['redirect_unauthorized']) && ! empty($this->config['redirect_unauthorized'])) { 41 | header(("Location: " . ($this->config['redirect_unauthorized'])), true, 301); 42 | exit(); 43 | } 44 | 45 | // otherwise send it to the local servers 403 page 46 | redirect('error/client_error/403'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/lib/munkireport/Listing.php: -------------------------------------------------------------------------------- 1 | listingData = $listingData; 15 | $this->template = 'listings/default'; 16 | return $this; 17 | } 18 | 19 | public function render($data = []) 20 | { 21 | if( ! $this->listingData){ 22 | $this->_renderPageNotFound(); 23 | } 24 | 25 | $data = [ 26 | 'page' => 'clients', 27 | 'scripts' => ["clients/client_list.js"], 28 | ] + $data; 29 | 30 | if( $this->_getType($this->listingData) == 'yaml'){ 31 | $this->_renderYAML($this->listingData, $data); 32 | }else{ 33 | $this->_renderPHP($this->listingData, $data); 34 | } 35 | } 36 | 37 | private function _renderPHP($listingData, $data) 38 | { 39 | view($listingData->view, $data, $listingData->view_path); 40 | } 41 | 42 | private function _renderYAML($listingData, $data) 43 | { 44 | $data = $data + Yaml::parseFile($this->_getPath($listingData, 'yml')); 45 | view($this->template, $data); 46 | } 47 | 48 | private function _renderPageNotFound() 49 | { 50 | $data = ['status_code' => 404]; 51 | $view = 'error/client_error'; 52 | view($view, $data); 53 | exit; 54 | } 55 | 56 | private function _getType($pathComponents) 57 | { 58 | return is_readable( $this->_getPath($pathComponents, 'yml')) ? 'yaml' : 'php'; 59 | } 60 | 61 | private function _getPath($pathComponents, $extension) 62 | { 63 | return $pathComponents->view_path . $pathComponents->view . '.' . $extension; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /database/migrations/2017_02_20_085316_machine_group.php: -------------------------------------------------------------------------------- 1 | hasTable($this->tableNameV2)) { 17 | // Migration already failed before, but didnt finish 18 | throw new Exception("previous failed migration exists"); 19 | } 20 | 21 | if ($capsule::schema()->hasTable($this->tableName)) { 22 | $capsule::schema()->rename($this->tableName, $this->tableNameV2); 23 | $migrateData = true; 24 | } 25 | 26 | $capsule::schema()->create($this->tableName, function (Blueprint $table) { 27 | $table->increments('id'); 28 | $table->integer('groupid')->nullable(); 29 | $table->string('property'); 30 | $table->string('value'); 31 | }); 32 | 33 | if ($migrateData) { 34 | $capsule::unprepared("INSERT INTO 35 | $this->tableName 36 | SELECT 37 | id, 38 | groupid, 39 | property, 40 | value 41 | FROM 42 | $this->tableNameV2"); 43 | } 44 | } 45 | 46 | public function down() 47 | { 48 | $capsule = new Capsule(); 49 | $capsule::schema()->dropIfExists($this->tableName); 50 | if ($capsule::schema()->hasTable($this->tableNameV2)) { 51 | $capsule::schema()->rename($this->tableNameV2, $this->tableName); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /database/migrations/0213_00_00_000001_cleanup_hash_table.php: -------------------------------------------------------------------------------- 1 | getLegacyModelSchemaVersion('hash'); 19 | $capsule = new Capsule(); 20 | 21 | if ($legacyVersion !== null && $legacyVersion < static::$legacySchemaVersion) { 22 | $rename_list = array( 23 | 'InstallHistory' => 'installhistory', 24 | 'Machine' => 'machine', 25 | 'InventoryItem' => 'inventory', 26 | 'inventoryitem' => 'inventory', 27 | 'Munkireport' => 'munkireport', 28 | 'Reportdata' => 'reportdata', 29 | 'filevault_status_model' => 'filevault_status', 30 | 'localadmin_model' => 'localadmin', 31 | 'network_model' => 'network', 32 | 'disk_report_model' => 'disk_report' 33 | ); 34 | 35 | foreach ($rename_list as $from => $to) { 36 | $capsule::table('hash') 37 | ->where('name', '=', $from) 38 | ->update(Array('name' => $to)); 39 | } 40 | 41 | $this->markLegacyMigrationRan(); 42 | } 43 | } 44 | 45 | // public function down() { 46 | // $legacyVersion = $this->getLegacyModelSchemaVersion('hash'); 47 | // 48 | // if ($legacyVersion == static::$legacySchemaVersion) { 49 | // 50 | // 51 | // $this->markLegacyRollbackRan(); 52 | // } 53 | // } 54 | } -------------------------------------------------------------------------------- /app/lib/munkireport/AuthLDAP.php: -------------------------------------------------------------------------------- 1 | config = $config; 12 | $this->groups = []; 13 | } 14 | 15 | public function login($login, $password) 16 | { 17 | $this->login = $login; 18 | 19 | if ($login && $password) { 20 | include_once(APP_PATH . '/lib/authLDAP/authLDAP.php'); 21 | $ldap_auth_obj = new \Auth_ldap($auth_data); 22 | if ($ldap_auth_obj->authenticate($login, $password)) { 23 | 24 | // Get groups 25 | if ($user_data = $ldap_auth_obj->getUserData($login)) { 26 | foreach($user_data['grps'] as $group){ 27 | $this->groups[] = $group; 28 | } 29 | } 30 | 31 | $auth_data = [ 32 | 'user' => $login, 33 | 'groups' => $this->groups, 34 | ]; 35 | 36 | if ($this->authorizeUserAndGroups($this->config, $auth_data)){ 37 | $this->authStatus = 'success'; 38 | return true; 39 | } 40 | 41 | $this->authStatus = 'unauthorized'; 42 | return false; 43 | } 44 | } 45 | 46 | return false; 47 | } 48 | 49 | public function getAuthMechanism() 50 | { 51 | return 'ldap'; 52 | } 53 | 54 | public function getAuthStatus() 55 | { 56 | return $this->authStatus; 57 | } 58 | 59 | public function getUser() 60 | { 61 | return $this->login; 62 | } 63 | 64 | public function getGroups() 65 | { 66 | return $this->groups; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /database/builders/MRQueryBuilder.php: -------------------------------------------------------------------------------- 1 | where($this->from.'.serial_number', $serial_number); 12 | } 13 | 14 | public function filter($what = '') 15 | { 16 | $this->filterMachineGroup(); 17 | if($what != 'groupOnly'){ 18 | $this->filterArchived(); 19 | } 20 | return $this; 21 | } 22 | 23 | private function filterMachineGroup() 24 | { 25 | $key = 'serial_number'; 26 | $table = 'reportdata'; 27 | if($this->from != $table){ 28 | $this->join( 29 | $table, 30 | $table.'.'.$key, 31 | '=', 32 | $this->from.'.'.$key 33 | ); 34 | } 35 | if ($groups = get_filtered_groups()) { 36 | $this->whereIn('machine_group', $groups); 37 | } 38 | } 39 | 40 | private function filterArchived() 41 | { 42 | if( is_archived_filter_on()) { 43 | $this->where('reportdata.archive_status', 0); 44 | }elseif( is_archived_only_filter_on() ){ 45 | $this->where('reportdata.archive_status', '!=', 0); 46 | } 47 | } 48 | 49 | public function insertChunked(array $values, int $chunkSize = 0) 50 | { 51 | if (empty($values)) { 52 | return true; 53 | } 54 | 55 | if (! is_array(reset($values))) { 56 | $values = [$values]; 57 | } 58 | 59 | if ( $chunkSize === 0 ){ 60 | // Calculate chunksize based on SQLite limits (max 999 inserts) 61 | $itemCount = count($values[0]); 62 | $chunkSize = floor(999 / $itemCount); 63 | } 64 | 65 | // Insert chunked data 66 | foreach( array_chunk ($values , $chunkSize, TRUE ) as $chunk ){ 67 | $this->insert($chunk); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/lib/munkireport/Email.php: -------------------------------------------------------------------------------- 1 | config = $config; 13 | } 14 | 15 | /** 16 | * Send email 17 | * 18 | * Send email 19 | * 20 | * @param array $email Email properties 21 | **/ 22 | public function send($email) 23 | { 24 | $out = array('error' => 0, 'error_msg' => ''); 25 | 26 | include_once(APP_PATH . '/lib/phpmailer/class.phpmailer.php'); 27 | include_once(APP_PATH . '/lib/phpmailer/class.smtp.php'); 28 | $mail = new \PHPMailer; 29 | 30 | // Get from 31 | list($from_addr, $from_name) = each($this->config['from']); 32 | 33 | $mail->isSMTP(); // Set mailer to use SMTP 34 | $mail->Host = $this->config['smtp_host']; // Specify main and backup SMTP servers 35 | $mail->SMTPAuth = $this->config['smtp_auth']; // Enable SMTP authentication 36 | $mail->Username = $this->config['smtp_username']; // SMTP username 37 | $mail->Password = $this->config['smtp_password']; // SMTP password 38 | $mail->SMTPSecure = $this->config['smtp_secure']; // Enable TLS encryption, `ssl` also accepted 39 | $mail->Port = $this->config['smtp_port']; // TCP port to connect to 40 | $mail->CharSet = "UTF-8"; 41 | $mail->setFrom($from_addr, $from_name); 42 | 43 | // Add recipient(s) 44 | foreach ($email['to'] as $to_addr => $to_name) { 45 | $mail->addAddress($to_addr, $to_name); 46 | } 47 | 48 | $mail->isHTML(true); // Set email format to HTML 49 | $mail->Subject = $email['subject']; 50 | $mail->Body = $email['content']; 51 | 52 | if (! $mail->send()) { 53 | $out['error'] = 1; 54 | $out['error_msg'] = $mail->ErrorInfo; 55 | } 56 | 57 | return $out; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/views/install/install_plist.php: -------------------------------------------------------------------------------- 1 | ' . "\n"; 7 | ?> 8 | 9 | 10 | 11 | autoremove 12 | 13 | catalogs 14 | 15 | testing 16 | 17 | name 18 | munkireport 19 | display_name 20 | Munkireport Install and config 21 | installer_type 22 | nopkg 23 | installs 24 | 25 | 26 | path 27 | /usr/local/munki/munkireport- 28 | type 29 | file 30 | 31 | 32 | preinstall_script 33 | #!/bin/bash 34 | /bin/bash -c "$(curl -s --max-time 10 index.php?/install)" 38 | minimum_os_version 39 | 10.9.0 40 | unattended_install 41 | 42 | uninstallable 43 | 44 | uninstall_method 45 | uninstall_script 46 | uninstall_script 47 | #!/bin/sh 48 | rm -rf /usr/local/munki/postflight \ 49 | /usr/local/munki/report_broken_client \ 50 | /usr/local/munki/munkilib/reportcommon.py* \ 51 | /usr/local/munki/munkireport-* \ 52 | /usr/local/munkireport/ \ 53 | /Library/MunkiReport/ \ 54 | /Library/LaunchDaemons/com.github.munkireport.runner.plist \ 55 | /Library/Preferences/MunkiReport.plist 56 | exit 0 57 | version 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /docs/autopkg.md: -------------------------------------------------------------------------------- 1 | AutoPkg for munkireport 2 | ===== 3 | 4 | If your munkireport server is running fine and you can create an install package from it, you can automate the process of creating installation packages using the munkireport autopkg recipe. This will help you create a new package suited for your organization when you update your munkireport server. 5 | 6 | Thi guide assumes you have installed [AutoPkg](https://github.com/autopkg/autopkg) and you have some experience working with AutoPkg. 7 | 8 | First add the munkireport recipe repository: 9 | 10 | ```sh 11 | autopkg repo-add munkireport-recipes 12 | ``` 13 | 14 | This will give you two recipes, munkireport.pkg.recipe and munkireport.munki.recipe. The first will create a package, the second will add it to your munki repository. You cannot use the recipes at the moment, because you have to add some information about your munkireport installation. To add that, you'll need to make an override: 15 | 16 | ```sh 17 | autopkg make-override munkireport.munki 18 | ``` 19 | 20 | AutoPkg has created an override file for you: ~/Library/AutoPkg/RecipeOverrides/munkireport.munki.recipe. Now you can edit the file with your favorite editor. 21 | 22 | The most important setting you have to change is 23 | 24 | ```xml 25 | BASEURL 26 | http://localhost:8888 27 | ``` 28 | 29 | This has to be the url that you use to access your munkireport server minus /index.php?etc. 30 | 31 | You could also change MUNKI_REPO_SUBDIR, NAME and modules. If you add modules here, you will force the modules that get packaged into the munkireport package, the modules you set in config.php will be ignored. Add modules in the following way: 32 | 33 | ```xml 34 | modules 35 | 36 | localadmin 37 | bluetooth 38 | 39 | ``` 40 | 41 | 42 | You can test your new recipe by running 43 | 44 | ```sh 45 | autopkg run -v munkireport.munki.recipe 46 | ``` 47 | 48 | If all goes well, you'll have imported a new Munkireport package into your munki repository. 49 | -------------------------------------------------------------------------------- /app/lib/munkireport/I18next.php: -------------------------------------------------------------------------------- 1 | locale = $locale ? $locale : 'en'; 22 | 23 | // Load the localisation JSON 24 | if ($json = @file_get_contents(PUBLIC_ROOT . 'assets/locales/' . $this->locale . '.json')) { 25 | $this->i18nArray = json_decode($json, true); 26 | } else { 27 | $this->i18nArray = array(); 28 | } 29 | } 30 | 31 | /** 32 | * Translate 33 | * * 34 | * @param type var Description 35 | **/ 36 | public function translate($text = '', $params = '') 37 | { 38 | $textArray = explode('.', $text); 39 | $search = $this->i18nArray; 40 | foreach ($textArray as $part) { 41 | if (! is_array($search) or ! isset($search[$part])) { 42 | return $text; 43 | } 44 | $parent = $search; 45 | $search = $parent[$part]; 46 | } 47 | 48 | if (is_array($search)) { 49 | return 'Array found for ' . $text; 50 | } 51 | 52 | // Check if there are params 53 | if ($params) { 54 | $paramsArray = json_decode($params, true); 55 | 56 | // Check if there is a count param 57 | if (isset($paramsArray['count']) && ($paramsArray['count'] == 0 || $paramsArray['count'] > 1 )) { 58 | if (isset($parent[$part.'_plural'])) { 59 | $search = $parent[$part.'_plural']; 60 | } 61 | } 62 | 63 | // Replace params 64 | foreach ($paramsArray as $find => $replace) { 65 | $search = str_replace('__'. $find . '__', $replace, $search); 66 | } 67 | } 68 | 69 | return $search; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /database/migrations/2017_12_18_173230_business_unit.php: -------------------------------------------------------------------------------- 1 | hasTable($this->tableNameV2)) { 17 | // Migration already failed before, but didnt finish 18 | throw new Exception("previous failed migration exists"); 19 | } 20 | 21 | if ($capsule::schema()->hasTable($this->tableName)) { 22 | $capsule::schema()->rename($this->tableName, $this->tableNameV2); 23 | $migrateData = true; 24 | } 25 | 26 | $capsule::schema()->create($this->tableName, function (Blueprint $table) { 27 | $table->increments('id'); 28 | $table->integer('unitid'); 29 | $table->string('property'); 30 | $table->string('value'); 31 | }); 32 | 33 | if ($migrateData) { 34 | $capsule::unprepared("INSERT INTO 35 | $this->tableName 36 | SELECT 37 | id, 38 | unitid, 39 | property, 40 | value 41 | FROM 42 | $this->tableNameV2"); 43 | $capsule::schema()->drop($this->tableNameV2); 44 | } 45 | 46 | // (Re)create indexes 47 | $capsule::schema()->table($this->tableName, function (Blueprint $table) { 48 | $table->index('property'); 49 | $table->index('value'); 50 | }); 51 | } 52 | 53 | public function down() 54 | { 55 | $capsule = new Capsule(); 56 | $capsule::schema()->dropIfExists($this->tableName); 57 | if ($capsule::schema()->hasTable($this->tableNameV2)) { 58 | $capsule::schema()->rename($this->tableNameV2, $this->tableName); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/lib/munkireport/ArrayToPlist.php: -------------------------------------------------------------------------------- 1 | parser = new CFPropertyList(); 21 | } 22 | 23 | public function parse($array) 24 | { 25 | $this->parser->add($dict = new \CFDictionary()); 26 | $this->addItem($dict, $array); 27 | 28 | return $this->parser->toXML(); 29 | } 30 | 31 | 32 | 33 | // Detect if this is an associative array 34 | public function has_string_keys(array $array) 35 | { 36 | return count(array_filter(array_keys($array), 'is_string')) > 0; 37 | } 38 | 39 | 40 | public function typeValue($value) 41 | { 42 | if (is_numeric($value)) { 43 | return new \CFNumber($value); 44 | } else { 45 | return new \CFString($value); 46 | } 47 | } 48 | 49 | public function addItem(&$dict, $item) 50 | { 51 | $parent = get_class($dict); 52 | 53 | //echo "$parent\n"; 54 | foreach ($item as $key => $value) { 55 | if (is_scalar($value)) { 56 | if ($parent == 'CFDictionary') { 57 | $dict->add($key, $this->typeValue($value)); 58 | } else { 59 | $dict->add($this->typeValue($value)); 60 | } 61 | } else { 62 | if ($this->has_string_keys($value)) { 63 | if ($parent == 'CFArray') { 64 | $dict->add($newdict = new \CFDictionary()); 65 | } else { 66 | $dict->add($key, $newdict = new \CFDictionary()); 67 | } 68 | } else { 69 | $dict->add($key, $newdict = new \CFArray()); 70 | } 71 | 72 | $this->addItem($newdict, $value); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /database/migrations/2017_02_12_235252_hash.php: -------------------------------------------------------------------------------- 1 | hasTable($this->tableNameV2)) { 17 | // Migration already failed before, but didnt finish 18 | throw new Exception("previous failed migration exists"); 19 | } 20 | 21 | if ($capsule::schema()->hasTable($this->tableName)) { 22 | $capsule::schema()->rename($this->tableName, $this->tableNameV2); 23 | $migrateData = true; 24 | } 25 | 26 | $capsule::schema()->create($this->tableName, function (Blueprint $table) { 27 | $table->increments('id'); 28 | $table->string('serial_number'); 29 | $table->string('name', 50); 30 | $table->string('hash'); 31 | $table->bigInteger('timestamp'); 32 | }); 33 | 34 | if ($migrateData) { 35 | $capsule::unprepared("INSERT INTO 36 | $this->tableName 37 | SELECT 38 | id, 39 | serial, 40 | name, 41 | hash, 42 | timestamp 43 | FROM 44 | $this->tableNameV2"); 45 | $capsule::schema()->drop($this->tableNameV2); 46 | } 47 | 48 | // (Re)create indexes 49 | $capsule::schema()->table($this->tableName, function (Blueprint $table) { 50 | $table->index(['serial_number']); 51 | $table->index(['serial_number', 'name']); 52 | }); 53 | } 54 | 55 | public function down() 56 | { 57 | $capsule = new Capsule(); 58 | $capsule::schema()->dropIfExists($this->tableName); 59 | if ($capsule::schema()->hasTable($this->tableNameV2)) { 60 | $capsule::schema()->rename($this->tableNameV2, $this->tableName); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /public/assets/client_installer/payload/usr/local/munki/report_broken_client: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Broken client reporter 4 | # First argument is the message 5 | # Second argument is the module that is reporting 6 | # Third argument is the severity (danger, warning, info) 7 | 8 | MSG="$1" 9 | if [ -z "$MSG" ]; then 10 | MSG="Unspecified client error" 11 | fi 12 | 13 | # Default module is reportbrokenclient 14 | MODULE="$2" 15 | if [ -z "$MODULE" ]; then 16 | MODULE="reportbrokenclient" 17 | fi 18 | 19 | # Default type is danger, other possible types: warning, info 20 | TYPE="$3" 21 | if [ -z "$TYPE" ]; then 22 | TYPE="danger" 23 | fi 24 | 25 | SERIAL=$(/usr/sbin/ioreg -c IOPlatformExpertDevice | /usr/bin/grep IOPlatformSerialNumber | /usr/bin/awk '{print $4}' | /usr/bin/tr -d '"') 26 | NAME=$(/usr/sbin/scutil --get ComputerName) 27 | OSVERSIONLONG=$(/usr/bin/uname -r) # Returns Darwin version 28 | OSVERS=${osversionlong/.*/} 29 | 30 | # Get pref from foundation if OS > 10.9 (Darwin 13) 31 | if [[ "${OSVERS}" > "13" ]] 32 | then 33 | BASEURL=$(/usr/bin/osascript -l JavaScript -e "ObjC.import('Foundation'); $.CFPreferencesCopyAppValue('BaseUrl', 'MunkiReport');") 34 | PASSPHRASE=$(/usr/bin/osascript -l JavaScript -e "ObjC.import('Foundation'); $.CFPreferencesCopyAppValue('Passphrase', 'MunkiReport');" 2>/dev/null) 35 | else 36 | BASEURL=$(/usr/bin/defaults read /Library/Preferences/MunkiReport BaseUrl) 37 | PASSPHRASE=$(/usr/bin/defaults read /Library/Preferences/MunkiReport Passphrase 2>/dev/null) 38 | fi 39 | SUBMITURL="${BASEURL}/index.php?/report/broken_client" 40 | 41 | # Application paths 42 | CURL="/usr/bin/curl" 43 | 44 | # Check if passphrase is set and submit it 45 | if [[ ${PASSPHRASE} == "" ]] ; then 46 | 47 | $CURL --max-time 5 --silent \ 48 | -d msg="$MSG" \ 49 | -d module="$MODULE" \ 50 | -d type="$TYPE" \ 51 | -d serial="$SERIAL" \ 52 | -d name="$NAME" \ 53 | "$SUBMITURL" 54 | 55 | # Has passphrase 56 | else 57 | 58 | $CURL --max-time 5 --silent \ 59 | -d msg="$MSG" \ 60 | -d module="$MODULE" \ 61 | -d type="$TYPE" \ 62 | -d serial="$SERIAL" \ 63 | -d passphrase="$PASSPHRASE" \ 64 | -d name="$NAME" \ 65 | "$SUBMITURL" 66 | fi 67 | 68 | exit 0 69 | -------------------------------------------------------------------------------- /app/views/dashboard/business_unit.php: -------------------------------------------------------------------------------- 1 | view('partials/head'); ?> 2 | 3 |
4 | 5 |
6 |
7 |
8 |
9 |

10 |
11 |
12 |
13 |

14 |
15 |
16 |
17 |
18 |
19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | view("widgets/${item}_widget"); ?> 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 |
35 | 36 | 91 | 92 | view('partials/foot'); ?> 93 | -------------------------------------------------------------------------------- /app/lib/munkireport/AbstractAuth.php: -------------------------------------------------------------------------------- 1 | valueToArray($auth_config['mr_allowed_users']); 25 | if (in_array(strtolower($auth_data['user']), array_map('strtolower', $admin_users))) { 26 | return true; 27 | } 28 | } 29 | // Check user against group list 30 | if ($checkGroups) { 31 | // Set mr_allowed_groups to array 32 | $admin_groups = $this->valueToArray($auth_config['mr_allowed_groups']); 33 | foreach ($auth_data['groups'] as $group) { 34 | if (in_array($group, $admin_groups)) { 35 | return true; 36 | } 37 | } 38 | }//end group list check 39 | 40 | return false; 41 | } 42 | 43 | /** 44 | * Convert value to array or keep Array 45 | * 46 | * @param mixed $value string or array 47 | * @return return array 48 | */ 49 | private function valueToArray($value='') 50 | { 51 | return is_array($value) ? $value : [$value]; 52 | } 53 | 54 | /** 55 | * Remove MunkiReport specific items from config array 56 | * 57 | * adldap2 trips over extra items in config array 58 | * 59 | * @param type $config Config array 60 | * @return return array 61 | */ 62 | public function stripMunkireportItemsFromConfig($config) 63 | { 64 | foreach(['mr_allowed_users', 'mr_allowed_groups', 'mr_recursive_groupsearch'] as $item){ 65 | unset($config[$item]); 66 | } 67 | return $config; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/controllers/Datatables.php: -------------------------------------------------------------------------------- 1 | authorized() || jsonError('Authenticate first', 403); 14 | 15 | // Connect to database 16 | $this->connectDB(); 17 | } 18 | 19 | public function data() 20 | { 21 | // Sanitize the GET variables here. 22 | $cfg = array( 23 | 'columns' => array(), 24 | 'order' => array(), 25 | 'start' => 0, // Start 26 | 'length' => -1, // Length 27 | 'draw' => 0, // Identifier, just return 28 | 'search' => '', // Search query 29 | 'where' => '', // Optional where clause 30 | 'mrColNotEmpty' => '', // Munkireport non empty column name 31 | 'mrColNotEmptyBlank' => '' // Munkireport non empty column name 32 | ); 33 | //echo '
';print_r($_GET);return;
34 | 
35 |         $searchcols = array();
36 | 
37 |         // Process $_POST array
38 |         foreach ($_POST as $k => $v) {
39 |             if ($k == 'search') {
40 |                 $cfg['search'] = $v['value'];
41 |             } elseif (isset($cfg[$k])) {
42 |                 $cfg[$k] = $v;
43 |             }
44 |         }// endforeach
45 | 
46 |         // Add columns to config
47 |         $cfg['search_cols'] = $searchcols;
48 | 
49 |         //echo '
';print_r($cfg);
50 | 
51 |         try {
52 |             // Get model
53 |             $obj = new Tablequery($cfg);
54 |             //echo '
';print_r($obj->fetch($cfg));
55 |             echo json_encode($obj->fetch($cfg));
56 | 
57 |             // Check for older php versions
58 |             if (function_exists('json_last_error')) {
59 |             // If there is an encoding error, show it
60 |                 if (json_last_error() != JSON_ERROR_NONE) {
61 |                     echo json_last_error_msg();
62 |                     print_r($obj->fetch($cfg));
63 |                 }
64 |             }
65 |         } catch (Exception $e) {
66 |             echo json_encode(array(
67 |                 'error' => $e->getMessage(),
68 |                 'draw' => intval($cfg['draw'])
69 |             ));
70 |         }
71 |     }
72 | }
73 | 


--------------------------------------------------------------------------------
/app/views/widgets/bargraph_widget.php:
--------------------------------------------------------------------------------
 1 | 
2 |
3 |
6 | data-i18n="[title]" 7 | 8 | > 9 |

10 | 11 | 12 | 13 |

14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 |
22 | 23 | 69 | -------------------------------------------------------------------------------- /app/Console/Commands/DownCommand.php: -------------------------------------------------------------------------------- 1 | comment('Application is already down.'); 39 | 40 | return true; 41 | } 42 | 43 | file_put_contents(storage_path('framework/down'), 44 | json_encode($this->getDownFilePayload(), 45 | JSON_PRETTY_PRINT)); 46 | 47 | $this->comment('Application is now in maintenance mode.'); 48 | } catch (Exception $e) { 49 | $this->error('Failed to enter maintenance mode.'); 50 | 51 | $this->error($e->getMessage()); 52 | 53 | return 1; 54 | } 55 | } 56 | 57 | /** 58 | * Get the payload to be placed in the "down" file. 59 | * 60 | * @return array 61 | */ 62 | protected function getDownFilePayload() 63 | { 64 | return [ 65 | 'time' => $this->currentTime(), 66 | 'message' => $this->option('message'), 67 | // 'retry' => $this->getRetryTime(), 68 | // 'allowed' => $this->option('allow'), 69 | ]; 70 | } 71 | 72 | /** 73 | * Get the number of seconds the client should wait before retrying their request. 74 | * 75 | * @return int|null 76 | */ 77 | protected function getRetryTime() 78 | { 79 | $retry = $this->option('retry'); 80 | 81 | return is_numeric($retry) && $retry > 0 ? (int) $retry : null; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /docs/hacking.md: -------------------------------------------------------------------------------- 1 | Hacking instructions for munkireport-php (at the moment just a braindump) 2 | 3 | ## Views 4 | 5 | All views are in the views folder, if you want to modify the views you can override the view path by adding 6 | 7 | $conf['view_path'] 8 | 9 | to config.php and point it to a different location outside the munkireport app directory. 10 | 11 | If you just want to change de dashboard, you can add a custom dashboard: 12 | 13 | app/views/dashboard/custom_dashboard.php 14 | 15 | ## Add more data 16 | 17 | To add new data to munkireport2 you can set up a module. A module is a directory that contains 18 | * an install script for the client (which will gather the appropriate data and point munkireport to it) 19 | * a model (which describes how the data is represented in the database) 20 | * optionally a controller (which you can use to download additional files) 21 | 22 | If you want to understand how modules work, take a look at some modules in the modules directory. 23 | 24 | ## Graphs 25 | 26 | Munkireport comes with a bundled graphing library: flotr2. 27 | 28 | Munkireport comes with a wrapper function around flotr2 that retrieves graph data and plots a graph. The wrapper is called DrawGraph(). 29 | 30 | There is also the NVD3 graphing library, based on D3.js. We want to move away from flotr for graphs. 31 | 32 | ### Network pie graph 33 | 34 | If you want to plot where your clients are in the network, you can use the network pie. Global network locations are in config.php. 35 | If you want to use those, just add an empty parameter object (parms = {}). 36 | 37 | // Override network settings in config.php 38 | var parms = { 39 | "Campus": ["145.108.", "130.37."] 40 | }; 41 | 42 | drawGraph("", '#ip-plot', pieOptions, parms); 43 | 44 | 45 | ## Variables 46 | 47 | Don't pollute the $GLOBALS array, at the moment there are a handful of variables passed around via $GLOBALS: 48 | 49 | * $GLOBALS['alerts'] - alerts and messages 50 | * $GLOBALS['auth'] - authorization variable (currently only in use for report) 51 | * $GLOBALS['conf'] - config items access with conf() 52 | * $GLOBALS['dbh'] - the database handle 53 | * $GLOBALS['version'] - the current version of MunkiReport 54 | 55 | # Javascript 56 | 57 | A large portion of the UI is based on javascript. 58 | 59 | ## Events 60 | 61 | When the DOM is ready and the language files are loaded, the appReady event is triggered. 62 | 63 | When there is an update in the filters or a refresh for a dashboard, appUpdate is triggered.. 64 | -------------------------------------------------------------------------------- /app/lib/munkireport/LegacyMigrationSupport.php: -------------------------------------------------------------------------------- 1 | capsule){ 12 | $this->capsule = new Capsule; 13 | 14 | if( ! $connection = conf('connection')){ 15 | die('Database connection not configured'); 16 | } 17 | 18 | if(has_mysql_db($connection)){ 19 | add_mysql_opts($connection); 20 | } 21 | 22 | $this->capsule->addConnection($connection); 23 | $this->capsule->setAsGlobal(); 24 | $this->capsule->bootEloquent(); 25 | } 26 | 27 | return $this->capsule; 28 | } 29 | 30 | /** 31 | * Query the `migration` table to retrieve the current model version for a MunkiReport PHP v2 model. 32 | * 33 | * @param $tableName string Name of the v2 table to check migrations for. 34 | * 35 | * @return integer The current version of the legacy table, or null if no such migration exists (model is either 36 | * older than v2 migrations or newer than v3) 37 | */ 38 | function getLegacyModelSchemaVersion($tableName) { 39 | $capsule = $this->connectDB(); 40 | if (!$capsule::schema()->hasTable('migration')) return null; 41 | 42 | $currentVersion = $capsule::table('migration') 43 | ->where('table_name', '=', $tableName) 44 | ->first(); 45 | 46 | if ($currentVersion) { 47 | return $currentVersion->version; 48 | } else { 49 | return null; 50 | } 51 | } 52 | 53 | function setLegacyModelSchemaVersion($tableName, $version) { 54 | $capsule = $this->connectDB(); 55 | $capsule::table('migration') 56 | ->where('table_name', $tableName) 57 | ->update(['version' => $version]); 58 | } 59 | 60 | function markLegacyMigrationRan() { 61 | $capsule = $this->connectDB(); 62 | $capsule::table('migration') 63 | ->where('table_name', static::$legacyTableName) 64 | ->update(['version' => static::$legacySchemaVersion]); 65 | } 66 | 67 | function markLegacyRollbackRan() { 68 | $capsule = $this->connectDB(); 69 | $capsule::table('migration') 70 | ->where('table_name', static::$legacyTableName) 71 | ->update(['version' => static::$legacySchemaVersion - 1]); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/views/system/widget_gallery.php: -------------------------------------------------------------------------------- 1 | view('partials/head'); ?> 2 | 3 | 6 | 7 |
8 | 9 |
10 |

11 | 12 | 13 | 14 | 15 |

16 |
17 | 18 | 19 | $data):?> 20 | 21 |
22 | 23 | 26 | 27 | 28 | 29 | view($this, $data['widget'], $data); ?> 30 | 31 | 32 | 33 | view($this, $item, $data); ?> 34 | 35 | 36 | 37 |
38 | 39 | 40 | 41 | 49 | 50 | 51 | 52 | type == 'yaml'):?> 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
42 | module; ?> 43 | active): ?> 44 | 45 | 46 | 47 | 48 |
view; ?>.ymlview; ?>.php
widget_file; ?>
63 |
64 |
65 | 66 | 67 | 68 |
69 | 70 | 71 | view('partials/foot'); ?> 72 | -------------------------------------------------------------------------------- /public/assets/css/bootstrap-markdown.min.css: -------------------------------------------------------------------------------- 1 | .md-editor{display:block;border:1px solid #ddd}.md-editor .md-footer,.md-editor>.md-header{display:block;padding:6px 4px;background:#f5f5f5}.md-editor>.md-header{margin:0}.md-editor>.md-preview{background:#fff;border-top:1px dashed #ddd;border-bottom:1px dashed #ddd;min-height:10px;overflow:auto}.md-editor>textarea{font-family:Menlo,Monaco,Consolas,"Courier New",monospace;font-size:14px;outline:0;margin:0;display:block;padding:0;width:100%;border:0;border-top:1px dashed #ddd;border-bottom:1px dashed #ddd;border-radius:0;box-shadow:none;background:#eee}.md-editor>textarea:focus{box-shadow:none;background:#fff}.md-editor.active{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.md-editor .md-controls{float:right;padding:3px}.md-editor .md-controls .md-control{right:5px;color:#bebebe;padding:3px 3px 3px 10px}.md-editor .md-controls .md-control:hover{color:#333}.md-editor.md-fullscreen-mode{width:100%;height:100%;position:fixed;top:0;left:0;z-index:99999;padding:60px 30px 15px;background:#fff!important;border:0!important}.md-editor.md-fullscreen-mode .md-footer{display:none}.md-editor.md-fullscreen-mode .md-input,.md-editor.md-fullscreen-mode .md-preview{margin:0 auto!important;height:100%!important;font-size:20px!important;padding:20px!important;color:#999;line-height:1.6em!important;resize:none!important;box-shadow:none!important;background:#fff!important;border:0!important}.md-editor.md-fullscreen-mode .md-preview{color:#333;overflow:auto}.md-editor.md-fullscreen-mode .md-input:focus,.md-editor.md-fullscreen-mode .md-input:hover{color:#333;background:#fff!important}.md-editor.md-fullscreen-mode .md-header{background:0 0;text-align:center;position:fixed;width:100%;top:20px}.md-editor.md-fullscreen-mode .btn-group{float:none}.md-editor.md-fullscreen-mode .btn{border:0;background:0 0;color:#b3b3b3}.md-editor.md-fullscreen-mode .btn.active,.md-editor.md-fullscreen-mode .btn:active,.md-editor.md-fullscreen-mode .btn:focus,.md-editor.md-fullscreen-mode .btn:hover{box-shadow:none;color:#333}.md-editor.md-fullscreen-mode .md-fullscreen-controls{position:absolute;top:20px;right:20px;text-align:right;z-index:1002;display:block}.md-editor.md-fullscreen-mode .md-fullscreen-controls a{color:#b3b3b3;clear:right;margin:10px;width:30px;height:30px;text-align:center}.md-editor.md-fullscreen-mode .md-fullscreen-controls a:hover{color:#333;text-decoration:none}.md-editor.md-fullscreen-mode .md-editor{height:100%!important;position:relative}.md-editor .md-fullscreen-controls{display:none}.md-nooverflow{overflow:hidden;position:fixed;width:100%} -------------------------------------------------------------------------------- /app/views/auth/create_local_user.php: -------------------------------------------------------------------------------- 1 | view('partials/head'); ?> 2 |
3 |
4 |
5 |
6 |
7 |
8 | 9 | 10 | view('partials/alerts'); ?> 11 |
12 | 15 |
16 | 17 |
18 |
19 |
20 | 23 |
24 | 25 |
26 |
27 |
28 |
29 | 31 | 32 | 33 |
34 |
35 | 36 |
37 |

38 | MunkiReport 39 | 40 | 41 | 42 |

43 |
44 |
45 |
46 |
47 |
48 | 49 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/views/auth/login.php: -------------------------------------------------------------------------------- 1 | view('partials/head'); ?> 2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | $list): ?> 33 | 34 | 35 | 36 |

37 | 38 | 39 | 40 | 41 | 42 |
43 | 44 |
45 | 46 |
47 |
48 |
49 | 50 |
51 | 52 |
53 |
54 | 55 | 56 |
57 |
58 | 59 |
60 |
61 | 62 | 63 | 64 |
65 |
66 | 67 |
68 |
69 | 70 | 71 | 72 |
73 |
74 |
75 |
76 |
77 |
78 | 79 | view('partials/foot', array('recaptcha' => true)); ?> 80 | -------------------------------------------------------------------------------- /app/controllers/Show.php: -------------------------------------------------------------------------------- 1 | authorized()) { 16 | redirect('auth/login'); 17 | } 18 | 19 | // Check for maintenance mode 20 | if(file_exists(APP_ROOT . 'storage/framework/down')) { 21 | redirect('error/client_error/503'); 22 | } 23 | 24 | $this->modules = $modules = getMrModuleObj()->loadInfo(); 25 | } 26 | 27 | public function index() 28 | { 29 | redirect('show/dashboard/default'); 30 | } 31 | 32 | public function dashboard($which = '') 33 | { 34 | if($which == '') 35 | { 36 | redirect('show/dashboard/default'); 37 | } 38 | $db = new Dashboard(conf('dashboard')); 39 | $db->render($which); 40 | } 41 | 42 | public function listing($module = '', $name = '') 43 | { 44 | $listing = new Listing($this->modules->getListing($module, $name)); 45 | $listing->render(); 46 | } 47 | 48 | public function report($module = '', $name = '') 49 | { 50 | $report = $this->modules->getReport($module, $name); 51 | 52 | if ( ! $report){ 53 | $this->_pageNotFound(); 54 | } 55 | 56 | if ($report->type == 'php') { 57 | view( 58 | $report->view, 59 | [ 60 | 'page' => 'clients', 61 | 'widget' => new Widgets(conf('widget')), 62 | ], 63 | $report->view_path 64 | ); 65 | }elseif ($report->type == 'yaml') { 66 | $db = new Dashboard([ 67 | 'search_paths' => [$report->view_path], 68 | 'template' => env('DASHBOARD_TEMPLATE', 'dashboard/dashboard'), 69 | 'default_layout' => [], 70 | ]); 71 | $db->render($report->view); 72 | } 73 | } 74 | 75 | public function custom($which = 'default') 76 | { 77 | if ( ! $which){ 78 | $this->_pageNotFound(); 79 | }else{ 80 | $this->_render($which, func_get_args(), APP_ROOT . 'custom/views/'); 81 | } 82 | } 83 | 84 | private function _render($view, $data, $viewpath) 85 | { 86 | view($view, $data, $viewpath); 87 | } 88 | 89 | private function _pageNotFound() 90 | { 91 | $data = array('status_code' => 404); 92 | $view = 'error/client_error'; 93 | $viewpath = conf('view_path'); 94 | view($view, $data, $viewpath); 95 | exit; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/controllers/Locale.php: -------------------------------------------------------------------------------- 1 | authorized()) { 16 | // header('Content-Type: application/json;charset=utf-8'); 17 | // echo '{"error": "Not Authorized"}'; 18 | // exit; 19 | // } 20 | 21 | $this->modules = getMrModuleObj()->loadInfo(); 22 | } 23 | 24 | /** 25 | * Get locale strings 26 | * 27 | * This function returns a JSON object containing 4 language objects and a messages object 28 | * The function just concatenates the various JSON files together instead of parsing 29 | * This is done for performance reasons. 30 | * 31 | * @param string $lang Locale definition, defaults to 'en' 32 | */ 33 | public function get($lang = 'en', $load = 'enabled_modules_only') 34 | { 35 | // Check if we should load all modules' locales 36 | if($load == 'all_modules'){ 37 | $this->modules = getMrModuleObj()->loadInfo(True); 38 | } 39 | 40 | $locales = array( 41 | 'messages' => '{}', 42 | 'fallback_main' => '{}', 43 | 'fallback_module' => '{}', 44 | 'lang_main' => '{}', 45 | 'lang_module' => '{}', 46 | ); 47 | 48 | // Load fallback language files 49 | $locales['fallback_main'] = file_get_contents(PUBLIC_ROOT.'assets/locales/'.$this->fallBackLang.'.json'); 50 | $locales['fallback_module'] = $this->modules->getModuleLocales($this->fallBackLang); 51 | 52 | if ($lang == $this->fallBackLang) { 53 | $locales['messages'] = '{"info": "requested language is fallback language"}'; 54 | } elseif ( ! preg_match($this->langFilter, $lang)) { 55 | $locales['messages'] = sprintf('{"error": "requested language is not valid: %s"}', $lang); 56 | } else { 57 | if (is_file(PUBLIC_ROOT.'assets/locales/'.$lang.'.json')) { 58 | $locales['lang_main'] = file_get_contents(PUBLIC_ROOT.'assets/locales/'.$lang.'.json'); 59 | } else { 60 | $locales['messages'] = sprintf('{"error": "Could not load main locale for: %s"}', $lang); 61 | } 62 | $locales['lang_module'] = $this->modules->getModuleLocales($lang); 63 | } 64 | 65 | // TODO: add ETAG support and caching. 66 | 67 | header('Content-Type: application/json;charset=utf-8'); 68 | echo "{\n"; 69 | foreach($locales as $key => $content){ 70 | printf('"%s": %s%s', $key, $content, $key != 'lang_module' ? ",\n": ''); 71 | } 72 | echo "}\n"; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/Console/Commands/MigrateCommand.php: -------------------------------------------------------------------------------- 1 | ensure_sqlite_db_exists($connection); 51 | } 52 | 53 | if(has_mysql_db($connection)){ 54 | add_mysql_opts($connection); 55 | } 56 | 57 | $capsule = new Capsule(); 58 | $capsule->addConnection($connection); 59 | $capsule->setAsGlobal(); 60 | $repository = new DatabaseMigrationRepository($capsule->getDatabaseManager(), 'migrations'); 61 | if ( ! $repository->repositoryExists()) { 62 | $repository->createRepository(); 63 | } 64 | 65 | $files = new \Illuminate\Filesystem\Filesystem(); 66 | $migrator = new Migrator($repository, $capsule->getDatabaseManager(), $files); 67 | 68 | $migrationDirList = [APP_ROOT . 'database/migrations']; 69 | 70 | // Add module migrations 71 | $moduleMgr = new ModuleMgr; 72 | $moduleMgr->loadinfo(true); 73 | foreach($moduleMgr->getInfo() as $moduleName => $info){ 74 | if($moduleMgr->getModuleMigrationPath($moduleName, $migrationPath)){ 75 | $migrationDirList[] = $migrationPath; 76 | } 77 | } 78 | 79 | $input = new \Symfony\Component\Console\Input\StringInput(''); 80 | $outputSymfony = new \Symfony\Component\Console\Output\ConsoleOutput(); 81 | $outputStyle = new \Illuminate\Console\OutputStyle($input, $outputSymfony); 82 | $migrationFiles = $migrator->setOutput($outputStyle)->run($migrationDirList, ['pretend' => false]); 83 | } 84 | 85 | private function ensure_sqlite_db_exists($connection) 86 | { 87 | touch($connection['database']); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /app/controllers/Filter.php: -------------------------------------------------------------------------------- 1 | authorized()) { 14 | redirect('auth/login'); 15 | } 16 | 17 | $this->registered_filters = [ 18 | 'machine_group' => [], 19 | 'archived' => ['yes'], 20 | 'archived_only' => [], 21 | ]; 22 | } 23 | 24 | /** 25 | * Add/remove a filter entry 26 | * 27 | * Currently only for machine_groups, but could contain 28 | * other filters (date, model, etc.) 29 | * 30 | **/ 31 | public function set_filter() 32 | { 33 | $out = []; 34 | 35 | $filter = $_POST['filter'] ?? ''; 36 | $action = $_POST['action'] ?? ''; 37 | $value = $_POST['value'] ?? ''; 38 | 39 | switch ($filter) { 40 | case 'machine_group': 41 | // Convert to int 42 | if (is_scalar($value)) { 43 | $value = intval($value); 44 | } 45 | break; 46 | case 'archived': 47 | break; 48 | case 'archived_only': 49 | break; 50 | default: 51 | jsonError('Unknown filter: '.$filter); 52 | } 53 | 54 | 55 | if (! isset($out['error'])) { 56 | // Create filter if it does not exist 57 | if (! isset($_SESSION['filter'][$filter])) { 58 | $_SESSION['filter'][$filter] = []; 59 | } 60 | 61 | // Find value in filter 62 | $key = array_search($value, $_SESSION['filter'][$filter]); 63 | 64 | // If key in filter: remove 65 | if ($key !== false) { 66 | array_splice($_SESSION['filter'][$filter], $key, 1); 67 | } 68 | 69 | switch ($action) { 70 | case 'add': // add to filter 71 | $_SESSION['filter'][$filter][] = $value; 72 | break; 73 | case 'add_all': // add to filter 74 | $_SESSION['filter'][$filter] = $value; 75 | break; 76 | case 'clear': // clear filter 77 | $_SESSION['filter'][$filter] = []; 78 | break; 79 | } 80 | 81 | // Return current filter array 82 | $out[$filter] = $_SESSION['filter'][$filter]; 83 | } 84 | 85 | jsonView($out); 86 | } 87 | 88 | /** 89 | * Get filters 90 | * 91 | **/ 92 | public function get_filter($filter = 'all') 93 | { 94 | if($filter == 'all'){ 95 | jsonView($this->_render_filter()); 96 | } 97 | } 98 | 99 | private function _render_filter() 100 | { 101 | return array_merge($this->registered_filters, $_SESSION['filter'] ?? []); 102 | } 103 | } -------------------------------------------------------------------------------- /app/models/Machine_group.php: -------------------------------------------------------------------------------- 1 | rs['id'] = ''; 14 | $this->rs['groupid'] = 0; 15 | $this->rs['property'] = ''; 16 | $this->rs['value'] = ''; 17 | 18 | $this->idx[] = array('property'); 19 | $this->idx[] = array('value'); 20 | 21 | // Table version. Increment when creating a db migration 22 | $this->schema_version = 0; 23 | 24 | // Create table if it does not exist 25 | //$this->create_table(); 26 | 27 | if ($groupid and $property) { 28 | $this->retrieveOne('groupid=? AND property=?', array($groupid, $property)); 29 | $this->groupid = $groupid; 30 | $this->property = $property; 31 | } 32 | 33 | return $this; 34 | } 35 | 36 | /** 37 | * Get max groupid 38 | * 39 | * @return integer max groupid 40 | * @author AvB 41 | **/ 42 | public function get_max_groupid() 43 | { 44 | $sql = 'SELECT MAX(groupid) AS max FROM '.$this->enquote($this->tablename); 45 | $result = $this->query($sql); 46 | return intval($result[0]->max); 47 | } 48 | 49 | /** 50 | * Select unique group ids 51 | * 52 | * @return void 53 | * @author 54 | **/ 55 | public function get_group_ids() 56 | { 57 | $out = array(); 58 | $sql = "SELECT groupid FROM $this->tablename GROUP BY groupid"; 59 | foreach ($this->query($sql) as $obj) { 60 | $out[] = $obj->groupid; 61 | } 62 | 63 | return $out; 64 | } 65 | 66 | // ------------------------------------------------------------------------ 67 | 68 | /** 69 | * Retrieve all entries for groupid 70 | * 71 | * @param integer groupid 72 | * @return array 73 | * @author abn290 74 | **/ 75 | public function all($groupid = '') 76 | { 77 | $out = array(); 78 | $where = $groupid !== '' ? 'groupid=?' : ''; 79 | 80 | foreach ($this->select('groupid, property, value', $where, $groupid, PDO::FETCH_OBJ) as $obj) { 81 | switch ($obj->property) { 82 | case 'key': 83 | $out[$obj->groupid]['keys'][] = $obj->value; 84 | break; 85 | default: 86 | $out[$obj->groupid][$obj->property] = $obj->value; 87 | } 88 | 89 | $out[$obj->groupid]['groupid'] = intval($obj->groupid); 90 | } 91 | 92 | if (! isset($obj)) { 93 | return array(); 94 | } 95 | 96 | if ($groupid !== '' && $out) { 97 | return $out[$groupid]; 98 | } else { 99 | return array_values($out); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/config/auth/saml.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'NameIDFormat' => env('AUTH_SAML_SP_NAME_ID_FORMAT', 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'), 6 | 'entityId' => env('AUTH_SAML_SP_ENTITY_ID', ''), 7 | 'x509cert' => env('AUTH_SAML_SP_X509CERT', ''), 8 | 'privateKey' => env('AUTH_SAML_SP_PRIVATEKEY', ''), 9 | ], 10 | 'idp' => [ 11 | 'entityId' => env('AUTH_SAML_IDP_ENTITY_ID', 'https://app.onelogin.com/saml/metadata/xxxx'), 12 | 'singleSignOnService' => [ 13 | 'url' => env('AUTH_SAML_IDP_SSO_URL', 'https://yourorg.onelogin.com/trust/saml2/http-post/sso/xxxx'), 14 | 'binding' => env('AUTH_SAML_IDP_SSO_BINDING', ''), 15 | ], 16 | 'singleLogoutService' => [ 17 | 'url' => env('AUTH_SAML_IDP_SLO_URL', 'https://yourorg.onelogin.com/trust/saml2/http-redirect/slo/xxxx'), 18 | 'binding' => env('AUTH_SAML_IDP_SLO_BINDING', ''), 19 | ], 20 | 'x509cert' => env('AUTH_SAML_IDP_X509CERT'), 21 | ], 22 | 'attr_mapping' => [ 23 | 'user' => env('AUTH_SAML_USER_ATTR', 'User.email'), 24 | 'groups' => env('AUTH_SAML_GROUP_ATTR', ['memberOf']), 25 | ], 26 | 'disable_sso' => env('AUTH_SAML_DISABLE_SSO', false), 27 | 'debug' => env('AUTH_SAML_DEBUG', false), 28 | 'security' => [ 29 | 'nameIdEncrypted' => env('AUTH_SAML_SECURITY_NAME_ID_ENCRYPTED', false), 30 | 'authnRequestsSigned' => env('AUTH_SAML_SECURITY_AUTHN_REQUESTS_SIGNED', false), 31 | 'logoutRequestSigned' => env('AUTH_SAML_SECURITY_LOGOUT_REQUEST_SIGNED', false), 32 | 'logoutResponseSigned' => env('AUTH_SAML_SECURITY_LOGOUT_RESPONSE_SIGNED', false), 33 | 'signMetadata' => env('AUTH_SAML_SECURITY_SIGN_METADATA', false), 34 | 'wantMessagesSigned' => env('AUTH_SAML_SECURITY_WANT_MESSAGES_SIGNED', false), 35 | 'wantAssertionsEncrypted' => env('AUTH_SAML_SECURITY_WANT_ASSERTIONS_ENCRYPTED', false), 36 | 'wantAssertionsSigned' => env('AUTH_SAML_SECURITY_WANT_ASSERTIONS_SIGNED', false), 37 | 'wantNameId' => env('AUTH_SAML_SECURITY_WANT_NAME_ID', true), 38 | 'wantNameIdEncrypted' => env('AUTH_SAML_SECURITY_WANT_NAME_ID_ENCRYPTED', false), 39 | 'requestedAuthnContext' => env('AUTH_SAML_SECURITY_REQUESTED_AUTHN_CONTEXT', true), 40 | 'wantXMLValidation' => env('AUTH_SAML_SECURITY_WANT_XML_VALIDATION', true), 41 | 'relaxDestinationValidation' => env('AUTH_SAML_SECURITY_RELAX_DESTINATION_VALIDATION', false), 42 | 'signatureAlgorithm' => env('AUTH_SAML_SECURITY_SIGNATURE_ALGORITHM', 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'), 43 | 'digestAlgorithm' => env('AUTH_SAML_SECURITY_DIGEST_ALGORITHM', 'http://www.w3.org/2001/04/xmlenc#sha256'), 44 | 'lowercaseUrlencoding' => env('AUTH_SAML_SECURITY_LOWERCASE_URLENCODING', false), 45 | ], 46 | 'munkireport' => [ 47 | 'mr_allowed_users' => env('AUTH_SAML_ALLOWED_USERS', []), 48 | 'mr_allowed_groups' => env('AUTH_SAML_ALLOWED_GROUPS', []), 49 | 'cert_directory' => env('AUTH_SAML_CERT_DIR', local_conf('certs/')), 50 | ], 51 | ]; 52 | -------------------------------------------------------------------------------- /docs/configure.md: -------------------------------------------------------------------------------- 1 | How to configure Munkireport 2 | ===== 3 | 4 | Munkireport is configured using a config file (config.php). 5 | 6 | You can also configure Munkireport using the environment, or using a `.env` file placed in the root 7 | which contains environment variables. See `.env.example` for environment settings. This is more common for 8 | docker deployments. 9 | 10 | **NOTE**: Configuration is loaded in this order: 11 | 12 | 1. `config_default.php` 13 | 2. `.env / ENVIRONMENT` 14 | 3. `config.php` 15 | 16 | Which means that `config.php` overwrites everything else. 17 | 18 | For the time being, read config_default.php. In the comments you'll find information about each setting. 19 | 20 | ## $conf['client_passphrases'] 21 | 22 | ### Default value: array() 23 | 24 | If you want to restrict access to your munkireport server, you can add passphrases to `$conf['client_passphrases']`. Munkireport will detect that the client_passphrases array is not empty and only accepts client requests that send the correct passphrase. 25 | If you set a passphrase on the server, you need to set the passphrase on the clients as well: 26 | 27 | ### Example 28 | 29 | On the server, add the following string to `config.php`: 30 | 31 | ```php 32 | $conf['client_passphrases'] = array('mysecretpassphrase') 33 | ``` 34 | 35 | On each client you need to add the passphrase to `MunkiReport.plist`: 36 | 37 | ```sh 38 | defaults write /Library/Preferences/MunkiReport Passphrase 'mysecretpassphrase' 39 | ``` 40 | 41 | Now when you run /usr/local/munki/postflight on the client, you should see that the update server can be contacted normally. 42 | 43 | ### Notes 44 | 45 | Munkireport will **not** set the passphrase on the client through the install script, it would be too easy for someone to get the passphrase that way. So you need to roll your own method of distributing the passphrase (via munki) 46 | 47 | ### Environment Variables 48 | 49 | - `CONNECTION_DRIVER`: Any driver that is valid for Illuminate/Database. Examples are `sqlite` and `mysql`. 50 | - `CONNECTION_DATABASE`: For sqlite: the path to the sqlite file. Otherwise, the name of the database. 51 | - `CONNECTION_HOST`: The hostname or IP address of the database server. 52 | - `CONNECTION_PORT`: The port to connect to (if non standard). 53 | - `CONNECTION_USERNAME`: The database connection username. 54 | - `CONNECTION_PASSWORD`: The database connection password. 55 | - `INDEX_PAGE`: The page appended to the app root, default is `index.php?`. 56 | - `URI_PROTOCOL`: Which server variable to use for the correct request path. Defaults to `Auto`. 57 | - `WEBHOST`: The URL to the server hosting the application, including the schema eg. `https://munkireport.local`. 58 | - `SUBDIRECTORY`: If your application is installed underneath a subdirectory, define this, eg. `/munkireport`. 59 | - `SITENAME`: The site name which will appear in the title bar of your browser, Default: `MunkiReport`. 60 | - `AUTH_METHODS`: A comma separated list of supported Authentication methods. Any combination of: 61 | - `NOAUTH`: No authentication required 62 | - `LDAP`: LDAP Authentication 63 | - `AD`: Active Directory Authentication 64 | -------------------------------------------------------------------------------- /app/controllers/Clients.php: -------------------------------------------------------------------------------- 1 | authorized()) { 15 | redirect('auth/login'); 16 | } 17 | 18 | // Connect to database 19 | $this->connectDB(); 20 | 21 | } 22 | 23 | public function index() 24 | { 25 | 26 | $data['page'] = 'clients'; 27 | 28 | view('client/client_list', $data); 29 | } 30 | 31 | /** 32 | * Get some data for serial_number 33 | * 34 | * @author AvB 35 | **/ 36 | public function get_data($serial_number = '') 37 | { 38 | if (authorized_for_serial($serial_number)) { 39 | $machine = new \Model; 40 | 41 | $sql = "SELECT m.computer_name, r.remote_ip, r.archive_status as status, n.ipv4ip, n.ipv6ip 42 | FROM machine m 43 | LEFT JOIN network n ON (m.serial_number = n.serial_number) 44 | LEFT JOIN reportdata r ON (m.serial_number = r.serial_number) 45 | WHERE m.serial_number = ? ORDER BY ipv4ip DESC LIMIT 1 46 | "; 47 | 48 | jsonView($machine->query($sql, $serial_number)); 49 | } else { 50 | jsonError('Not authorized for serial number', 403, false); 51 | } 52 | } 53 | 54 | /** 55 | * Retrieve links from config 56 | * 57 | * @author 58 | **/ 59 | public function get_links() 60 | { 61 | $out = array(); 62 | if (conf('vnc_link')) { 63 | $out['vnc'] = conf('vnc_link'); 64 | } 65 | if (conf('ssh_link')) { 66 | $out['ssh'] = conf('ssh_link'); 67 | } 68 | 69 | jsonView($out); 70 | } 71 | 72 | // ------------------------------------------------------------------------ 73 | 74 | /** 75 | * Detail page of a machine 76 | * 77 | * @param string serial 78 | * @return void 79 | * @author abn290 80 | **/ 81 | public function detail($sn = '') 82 | { 83 | $data = array('serial_number' => $sn); 84 | $data['scripts'] = array("clients/client_detail.js"); 85 | 86 | 87 | $machine = Machine_model::where('serial_number', $sn) 88 | ->first(); 89 | 90 | // Check if machine exists/is allowed for this user to view 91 | if (! $machine) { 92 | view("client/client_dont_exist", $data); 93 | } else { 94 | view("client/client_detail", $data); 95 | } 96 | } 97 | 98 | // ------------------------------------------------------------------------ 99 | 100 | /** 101 | * List of machines 102 | * 103 | * @param string name of view 104 | * @return void 105 | * @author abn290 106 | **/ 107 | public function show($view = '') 108 | { 109 | $data['page'] = 'clients'; 110 | // TODO: Check if view exists 111 | view('client/'.$view, $data); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/lib/munkireport/Request.php: -------------------------------------------------------------------------------- 1 | options = [ 19 | 'headers' => [ 20 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15', 21 | ], 22 | 'timeout' => conf('request_timeout', 5) 23 | ]; 24 | 25 | // Choose http handler 26 | switch (conf('guzzle_handler')) { 27 | case 'stream': 28 | $handler = new StreamHandler(); 29 | $stack = HandlerStack::create($handler); 30 | $this->options['handler'] = $stack; 31 | break; 32 | 33 | case 'curl': 34 | $handler = new CurlHandler(); 35 | $stack = HandlerStack::create($handler); 36 | $this->options['handler'] = $stack; 37 | break; 38 | 39 | default: 40 | // Use automatic 41 | break; 42 | } 43 | 44 | // Add proxy 45 | $proxy = conf('proxy'); 46 | if (isset($proxy['server']) && $proxy['server']) { 47 | $proxy['server'] = str_replace('tcp://', '', $proxy['server']); 48 | $proxy['port'] = isset($proxy['port']) ? $proxy['port'] : 8080; 49 | $this->options['proxy'] = 'http://' . $proxy['server'].':'.$proxy['port']; 50 | 51 | // Authenticated proxy 52 | if (isset($proxy['username']) && isset($proxy['password'])) { 53 | // Encode username and password 54 | $auth = base64_encode($proxy['username'].':'.$proxy['password']); 55 | $this->options['headers']["Proxy-Authorization"] = "Basic $auth"; 56 | } 57 | } 58 | } 59 | 60 | public function get($url, $options = []) 61 | { 62 | return $this->request('GET', $url, $options); 63 | } 64 | 65 | public function post($url, $options = []) 66 | { 67 | return $this->request('POST', $url, $options); 68 | } 69 | 70 | private function request($type, $url, $options = []) 71 | { 72 | $client = new Client(); 73 | try { 74 | $response = $client->request($type, $url, array_merge($this->options, $options)); 75 | return $response->getBody(); 76 | } catch (TransferException $e) { 77 | if(conf('debug')){ 78 | $this->dump_exception($e); 79 | exit(); 80 | } 81 | } 82 | } 83 | 84 | private function dump_exception($e) 85 | { 86 | printf("
ERROR: %s
", htmlentities($e->getMessage())); 87 | printf("
REQUEST:\n%s
", Psr7\str($e->getRequest())); 88 | if ($e->hasResponse()) { 89 | printf("
RESPONSE:\n%s
", htmlentities(Psr7\str($e->getResponse()))); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | MunkiReport 3 | =============== 4 | 5 | MunkiReport is a reporting client for macOS. While originally dependent on [Munki](https://github.com/munki/munki/), MunkiReport is able to run stand-alone or to be coupled with Munki, Jamf or other macOS management solutions. 6 | 7 | ![Dashboard view](https://github.com/munkireport/munkireport-php/wiki/assets/pics/dashboard.png) 8 | 9 | Features 10 | --- 11 | 12 | * Quick overview of your macOS fleet with a customizable dashboards 13 | * Get reports on many features (hardware types, disk usage, etc) 14 | * Extendable with a growing list of [modules](https://github.com/munkireport/munkireport-php/wiki/Modules) 15 | * Lightweight: only sends reports when facts have changed 16 | * Responsive webdesign 17 | * [more features](https://github.com/munkireport/munkireport-php/wiki/Features) 18 | 19 | Setup 20 | --- 21 | 22 | Setup is easy, you could be running your own reporting server within minutes! 23 | 24 | Please read the [demonstration setup](https://github.com/munkireport/munkireport-php/wiki/Quick-demo). 25 | 26 | System Requirements 27 | --- 28 | 29 | ### Serverside: 30 | 31 | * A webserver (runs fine with Apache, IIS and nginx) 32 | * php version 8.1 or higher with pdo-sqlite3 and libxml 33 | 34 | ### Clientside 35 | 36 | * a Modern webbrowser 37 | 38 | Support 39 | --- 40 | 41 | For security-related issues, please use the private mailing list: 42 | 43 | https://groups.google.com/group/munkireport-security 44 | 45 | Otherwise, the MunkiReport community can be found on the Mac Admins Slack: 46 | 47 | https://www.macadmins.org 48 | 49 | For questions about using MunkiReport or setting up MunkiReport, join us in the #munkireport channel. 50 | 51 | For developers who want to contribute to the project, join us in the #munkireport-dev channel. 52 | 53 | Contributing 54 | --- 55 | 56 | If you want to contribute to MunkiReport, please 57 | 58 | * read about [Localizing](docs/localize.md) in the docs folder 59 | * check the [modules overview](https://github.com/munkireport/munkireport-php/wiki/Module-Overview) for info about installing and creating modules 60 | * fork the [wip branch of repository](https://github.com/munkireport/munkireport-php/tree/wip) 61 | * create a feature branch 62 | * send a pull request with your changes 63 | 64 | External projects 65 | --- 66 | 67 | MunkiReport makes use of these fine software packages: 68 | 69 | * [php](http://php.net) serverside scripting 70 | * [CFPropertyList](https://github.com/rodneyrehm/CFPropertyList) serverside plist parsing 71 | * [phpass 0.3.5](https://github.com/hautelook/phpass) for encrypting passwords 72 | * [phpserialize](https://github.com/sdfsdhgjkbmnmxc/phpserialize) for serializing client data 73 | * [jQuery](http://jquery.com) for easy javascript 74 | * [Datatables](http://datatables.net) table display 75 | * [nvd3](https://github.com/nvd3-community/nvd3) for graphs 76 | * [Moment.js](http://momentjs.com) for displaying time 77 | * [Bootstrap 3.0](http://getbootstrap.com) the main webframework 78 | * [Font Awesome](http://fortawesome.github.io/Font-Awesome/) for icons 79 | * [adLDAP](https://github.com/Adldap2/Adldap2) for authenticating against AD 80 | * [i18next](http://i18next.com) js library for localization 81 | -------------------------------------------------------------------------------- /app/helpers/config_helper.php: -------------------------------------------------------------------------------- 1 | load(); 17 | } catch (InvalidPathException $e) { 18 | // .env is missing, but not really an issue since configuration is specified here anyway. 19 | } catch (InvalidFileException $e) { 20 | die($e->getMessage()); 21 | } 22 | 23 | } 24 | 25 | function loadAuthConfig() 26 | { 27 | $auth_config = []; 28 | foreach (env('AUTH_METHODS', []) as $auth_method) { 29 | switch (strtoupper($auth_method)) { 30 | case 'NOAUTH': 31 | $auth_config['auth_noauth'] = require APP_ROOT . 'app/config/auth/noauth.php'; 32 | break; 33 | case 'ENV': 34 | $auth_config['auth_env'] = require APP_ROOT . 'app/config/auth/env.php'; 35 | break; 36 | case 'SAML': 37 | $auth_config['auth_saml'] = require APP_ROOT . 'app/config/auth/saml.php'; 38 | break; 39 | case 'LOCAL': 40 | $auth_config['auth_local'] = require APP_ROOT . 'app/config/auth/local.php'; 41 | break; 42 | case 'LDAP': 43 | $auth_config['auth_ldap'] = require APP_ROOT . 'app/config/auth/ldap.php'; 44 | break; 45 | case 'AD': 46 | $auth_config['auth_AD'] = require APP_ROOT . 'app/config/auth/ad.php'; 47 | break; 48 | case 'NETWORK': 49 | $auth_config['network'] = require APP_ROOT . 'app/config/auth/network.php'; 50 | break; 51 | } 52 | } 53 | configAppendArray($auth_config, 'auth'); 54 | } 55 | 56 | function initConfig() 57 | { 58 | $GLOBALS['conf'] = []; 59 | } 60 | 61 | /** 62 | * Add config array to global config array 63 | * 64 | * 65 | * @param array $configArray 66 | */ 67 | function configAppendArray($configArray, $namespace = '') 68 | { 69 | if($namespace){ 70 | $GLOBALS['conf'] += [$namespace => $configArray]; 71 | } 72 | else{ 73 | $GLOBALS['conf'] += $configArray; 74 | } 75 | } 76 | 77 | /** 78 | * Add config file to global config array 79 | * 80 | * 81 | * @param array $configPath 82 | */ 83 | function configAppendFile($configPath, $namespace = '') 84 | { 85 | $config = require $configPath; 86 | configAppendArray($config, $namespace); 87 | } 88 | 89 | /** 90 | * Get config item 91 | * @param string config item 92 | * @param string default value (optional) 93 | * @author AvB 94 | **/ 95 | function conf($cf_item, $default = '') 96 | { 97 | return array_key_exists($cf_item, $GLOBALS['conf']) ? $GLOBALS['conf'][$cf_item] : $default; 98 | } 99 | 100 | function local_conf($item) 101 | { 102 | return rtrim(conf('local'), '/') . '/' . $item; 103 | } 104 | 105 | function module_conf($item) 106 | { 107 | return local_conf('module_configs/' .$item); 108 | } -------------------------------------------------------------------------------- /app/Console/Commands/SeedCommand.php: -------------------------------------------------------------------------------- 1 | ensure_sqlite_db_exists($connection); 51 | } 52 | 53 | if(has_mysql_db($connection)){ 54 | add_mysql_opts($connection); 55 | } 56 | 57 | $capsule = new Capsule(); 58 | $capsule->addConnection($connection); 59 | $capsule->setAsGlobal(); 60 | $capsule->bootEloquent(); 61 | 62 | $this->comment("Creating fake database records..."); 63 | 64 | $faker = \Faker\Factory::create($this->option('locale')); 65 | $factory = new MrFactory($faker); 66 | $moduleMgr = new ModuleMgr; 67 | $moduleMgr->loadinfo(true); 68 | 69 | $factory_models = []; 70 | foreach($moduleMgr->getInfo() as $moduleName => $info){ 71 | // print("Finding model factories in " . $moduleMgr->getPath($moduleName). "\n"); 72 | $factorypath = $moduleMgr->getPath($moduleName, ("/".$moduleName."_factory.php")); 73 | 74 | if(is_file($factorypath)){ 75 | $factory->load($factorypath); 76 | $factory_models[] = ucfirst($moduleName . "_model"); 77 | } 78 | } 79 | 80 | // Create ReportData first 81 | $reportData = $factory->of(\Reportdata_model::class)->times($this->option('records')); 82 | $this->deleteFromArray($factory_models, 'Reportdata_model'); 83 | 84 | // Process all other modules 85 | foreach ($reportData->create() as $r) { 86 | foreach($factory_models as $model){ 87 | $factory->of($model)->create(['serial_number' => $r->serial_number]); 88 | } 89 | } 90 | 91 | } 92 | 93 | private function ensure_sqlite_db_exists($connection) 94 | { 95 | touch($connection['database']); 96 | } 97 | 98 | private function deleteFromArray(&$array, $value){ 99 | if (($key = array_search($value, $array)) !== false) { 100 | unset($array[$key]); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /docs/authorization.md: -------------------------------------------------------------------------------- 1 | # Authorization, Roles and Groups 2 | 3 | MunkiReport uses Role Based authorization model, which means that users can do things based on the role they have. Any user can have only one role. 4 | At the moment there are 4 roles defined: 5 | 6 | * Admin 7 | * Manager 8 | * Archiver 9 | * User 10 | * Nobody 11 | 12 | ## No Business Units 13 | 14 | When Business Units are **not** configured, the following authorizations apply. 15 | A user that does not have an admin-role or manager-role gets the role of **user**. 16 | 17 | Role | View | Delete Machine | Archive Machine 18 | ------- | ------------ | -------------- | -------------- 19 | admin | All machines | Yes | Yes 20 | manager | All machines | Yes | Yes 21 | archiver| All machines | No | Yes 22 | user | All machines | No | No 23 | 24 | 25 | ## Business Units 26 | 27 | When Business Units are enabled, the roles change a little bit. 28 | A user that does not have an admin role and is not found an a business unit gets the role of **nobody**. 29 | 30 | Role | View | Delete Machine | Archive Machine | Edit Business Units 31 | ------- | ------------ | -------------- | -------------- | ------------------- 32 | admin | All machines | Yes | Yes | Yes 33 | manager | BU only | BU only | Yes | No 34 | archiver| BU only | No | Yes | No 35 | user | BU only | No | No | No 36 | nobody | No machines | No | No | No 37 | 38 | 39 | ## Add role to a user 40 | 41 | By default all users have the admin role, due to a setting in config_default.php. To override this setting, create the following in config.php: 42 | 43 | ```php 44 | $conf['roles']['admin'] = array('your_username'); 45 | ``` 46 | 47 | This will give 'your_username' the role of admin. 48 | You can also add groups to a role array: 49 | 50 | ```php 51 | $conf['roles']['admin'] = array('your_username', '@admin_group'); 52 | ``` 53 | 54 | This will give all users in the group 'admin_group' the role of admin. Groups can be local groups, LDAP groups or AD groups, make sure you prefix the groupname with @. 55 | 56 | ## Local groups 57 | 58 | To make a local group, add the following to config.php: 59 | 60 | ```php 61 | $conf['groups']['admin_group'] = array('your_username'); 62 | ``` 63 | 64 | To reference this group in the roles array, prefix the name with @. You can also use this group in Business Units. At the moment, it is not possible to nest groups 65 | 66 | ## View session variables 67 | 68 | If you want to see the actual authorization settings, and the reason a user got a certain role, you can view the current settings here: 69 | 70 | ``` 71 | http://example.com/index.php?/auth/set_session_props/1 72 | ``` 73 | 74 | ## Authorizations (topic for developers) 75 | 76 | There are two authorizations enabled: 77 | 78 | * global - view everything 79 | * delete_machine - be able to delete a machine from the database 80 | 81 | By default, users with the **admin** role have the 'global' and the 'delete_machine' authorization. users with the **manager** role only have the 'delete_machine' authorization. 82 | You can override the authorizations in config.php, but don't do that unless you know what you are doing! 83 | Developers can use the $conf['authorization'] array to create new authorizations based on role. 84 | -------------------------------------------------------------------------------- /app/controllers/Unit.php: -------------------------------------------------------------------------------- 1 | authorized()) { 14 | redirect('auth/login'); 15 | } 16 | } 17 | 18 | public function index() 19 | { 20 | $data = array('session' => $_SESSION); 21 | 22 | echo 'BU dashboard
';
 23 | 
 24 |         print_r($_SESSION);
 25 |         return;
 26 |     }
 27 | 
 28 |     /**
 29 |      * Get unit data for current user
 30 |      *
 31 |      * @author
 32 |      **/
 33 |     public function get_data()
 34 |     {
 35 |         $out = array();
 36 | 
 37 |         // Initiate session
 38 |         $this->authorized();
 39 | 
 40 |         if (isset($_SESSION['business_unit'])) {
 41 |         // Get data for this unit
 42 |             $unit = new Business_unit;
 43 |             $out = $unit->all($_SESSION['business_unit']);
 44 |         }
 45 | 
 46 |         jsonView($out);
 47 |     }
 48 | 
 49 |     /**
 50 |      * Get machine group data for current user
 51 |      *
 52 |      * @author
 53 |      **/
 54 |     public function get_machine_groups()
 55 |     {
 56 |         $out = array();
 57 | 
 58 |         if (isset($_SESSION['machine_groups'])) {
 59 |         // Get data for this unit
 60 |             $mg = new Machine_group;
 61 |             foreach ($_SESSION['machine_groups'] as $group) {
 62 |                 if ($mg_data = $mg->all($group)) {
 63 |                     $out[] = $mg->all($group);
 64 |                 } else if ($group != 0 && count($_SESSION['machine_groups']) != 0) // Not in Machine_group table
 65 |                 {
 66 |                     $out[] = array(
 67 |                     'name' => 'Group '.$group,
 68 |                     'groupid' => $group);
 69 |                 } else {
 70 |                     $out[] = array(
 71 |                     'name' => 'Unassigned',
 72 |                     'groupid' => $group);
 73 |                 }
 74 |             }
 75 |         } else {
 76 |             $mg = new Machine_group;
 77 |             $out = $mg->all();
 78 |         }
 79 | 
 80 |         //Apply filter
 81 |         $groups = get_filtered_groups();
 82 |         foreach ($out as &$group) {
 83 |             $group['checked'] = in_array($group['groupid'], $groups);
 84 |         }
 85 | 
 86 |         usort($out, function($a, $b) {
 87 |             return strcasecmp($a['name'], $b['name']);
 88 |         });
 89 |         
 90 |         jsonView($out);
 91 |     }
 92 | 
 93 |     public function listing($which = '')
 94 |     {
 95 |         if ($which) {
 96 |             $data['page'] = 'clients';
 97 |             $data['scripts'] = array("clients/client_list.js");
 98 |             $view = 'listing/'.$which;
 99 |         } else {
100 |             $data = array('status_code' => 404);
101 |             $view = 'error/client_error';
102 |         }
103 | 
104 |         view($view, $data);
105 |     }
106 | 
107 |     public function reports($which = 'default')
108 |     {
109 |         if ($which) {
110 |             $data['page'] = 'clients';
111 |             $view = 'report/'.$which;
112 |         } else {
113 |             $data = array('status_code' => 404);
114 |             $view = 'error/client_error';
115 |         }
116 | 
117 |         view($view, $data);
118 |     }
119 | }
120 | 


--------------------------------------------------------------------------------
/app/views/widgets/scrollbox_widget.php:
--------------------------------------------------------------------------------
 1 | 
2 |
3 |
6 | data-i18n="[title]" 7 | 8 | data-container="body"> 9 |

10 | 11 | 12 | 13 |

14 |
15 |
16 |
17 |
18 | 19 | 82 | -------------------------------------------------------------------------------- /app/lib/munkireport/AuthLocal.php: -------------------------------------------------------------------------------- 1 | config = $config; 15 | $this->mechanism = 'local'; 16 | $this->users = []; 17 | $this->loadUsers(); 18 | } 19 | 20 | public function login($login, $password) 21 | { 22 | $this->authStatus = 'not_found'; 23 | $this->login = $login; 24 | 25 | if ($login){ 26 | $storedPassword = $this->findHashForLogin($login); 27 | 28 | if ($storedPassword) { 29 | $t_hasher = $this->load_phpass(); 30 | if ($t_hasher->CheckPassword($password, $storedPassword)) { 31 | $this->authStatus = 'success'; 32 | return true; 33 | } 34 | $this->authStatus = 'failed'; 35 | } 36 | } 37 | 38 | return false; 39 | } 40 | 41 | public function getAuthMechanism() 42 | { 43 | return $this->mechanism; 44 | } 45 | 46 | public function getAuthStatus() 47 | { 48 | return $this->authStatus; 49 | } 50 | 51 | public function getUser() 52 | { 53 | return $this->login; 54 | } 55 | 56 | public function getGroups() 57 | { 58 | $groups = []; 59 | foreach (conf('groups', array()) as $groupname => $members) { 60 | if (in_array($this->login, $members)) { 61 | $groups[] = $groupname; 62 | } 63 | } 64 | return $groups; 65 | } 66 | 67 | private function findHashForLogin($login) 68 | { 69 | return isset($this->users[$login]['password_hash']) ? $this->users[$login]['password_hash'] : false; 70 | } 71 | 72 | public function load_phpass() 73 | { 74 | return new PasswordHash(8, true); 75 | } 76 | 77 | private function isYaml($file) 78 | { 79 | return pathinfo($file, PATHINFO_EXTENSION) == 'yml'; 80 | } 81 | 82 | private function fullPath($dir, $file) 83 | { 84 | return rtrim($dir, '/') . '/' . $file; 85 | } 86 | 87 | private function getName($file) 88 | { 89 | return pathinfo($file, PATHINFO_FILENAME); 90 | } 91 | 92 | private function loadUsers() 93 | { 94 | foreach($this->config['search_paths'] as $user_path){ 95 | if(! is_dir($user_path)){ 96 | continue; 97 | } 98 | 99 | foreach(scandir($user_path) AS $file) 100 | { 101 | $full_path = $this->fullPath($user_path, $file); 102 | if(! $this->isYaml($full_path)) 103 | { 104 | continue; 105 | } 106 | try { 107 | $user_data = Yaml::parseFile($full_path); 108 | if(isset($user_data['password_hash'])){ 109 | $this->users[$this->getName($full_path)] = $user_data; 110 | } 111 | } catch (\Exception $e) { 112 | // Do something 113 | } 114 | } 115 | } 116 | } 117 | 118 | } -------------------------------------------------------------------------------- /app/views/widgets/button_widget.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
6 | data-i18n="[title]" 7 | 8 | data-container="body"> 9 |

10 | 11 | 12 | 13 |

14 |
15 |
16 |
17 |
18 | 19 | 96 | -------------------------------------------------------------------------------- /app/controllers/Module.php: -------------------------------------------------------------------------------- 1 | moduleManager = getMrModuleObj(); 25 | } 26 | 27 | public function index() 28 | { 29 | } 30 | 31 | 32 | public function load() 33 | { 34 | //Parse request (determine controller/action/params) 35 | $this->params = array(); 36 | $p = func_get_args(); 37 | if (isset($p[0]) && $p[0]) { 38 | $this->module=$p[0]; 39 | } 40 | if (isset($p[1]) && $p[1]) { 41 | $this->action=$p[1]; 42 | } 43 | if (isset($p[2])) { 44 | $this->params=array_slice($p, 2); 45 | } 46 | 47 | if (! preg_match('#^[A-Za-z0-9_-]+$#', $this->module)){ 48 | $this->requestNotFound('illegal module name: '.$this->module); 49 | } 50 | 51 | //Route request to correct controller/action 52 | 53 | if (! $this->moduleManager->getmoduleControllerPath($this->module, $module_file)) { 54 | $this->requestNotFound('Module controller not found: '.$this->module); 55 | } 56 | 57 | //Create module obj 58 | require($module_file); 59 | $this->module_classname = '\\' . $this->module.'_controller'; 60 | if (! class_exists($this->module_classname, false)) { 61 | $this->requestNotFound('Module class not found: '.$this->module_classname); 62 | } 63 | $this->module_obj = new $this->module_classname; 64 | 65 | //call controller function 66 | if (! preg_match('#^[A-Za-z_][A-Za-z0-9_-]*$#', $this->action) or ! method_exists($this->module_obj, $this->action)) { 67 | $this->requestNotFound('Invalid method name: '.$this->action); 68 | } 69 | 70 | // These methods don't require authentication 71 | $unProtectedActions = ["get_script", "index"]; 72 | // Require authentication for all methods 73 | if( ! in_array($this->action, $unProtectedActions) && ! $this->module_obj->authorized()) 74 | { 75 | $this->requestForbidden('Module controller filter'); 76 | } 77 | 78 | // Connect to database 79 | $this->module_obj->connectDBWhenAuthorized(); 80 | 81 | call_user_func_array(array( $this->module_obj, $this->action ), $this->params); 82 | } 83 | 84 | //Override this function for your own custom 404 page 85 | public function requestNotFound($msg = '') 86 | { 87 | header("HTTP/1.0 404 Not Found"); 88 | die('404 Not Found

Not Found

'.$msg.'

The requested URL was not found on this server.

Please go back and try again.


Powered By: KISSMVC

'); 89 | } 90 | 91 | public function requestForbidden($msg = '') 92 | { 93 | header("HTTP/1.0 403 Forbidden"); 94 | die('403 Forbidden

Forbidden

'.$msg.'

The requested URL was allowed on this server.

Please authenticate first and try again.


'); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /tests/fixtures/env.fixture: -------------------------------------------------------------------------------- 1 | # Fixture ENV file to load in testing, to make sure that all options have been affected by the .env loading. 2 | # Unit tests will test all string values for "FOOBAR" 3 | # All boolean values will be true 4 | # All integer values will be 99 5 | # All array values will be an array of length 2 ["FOO", "BAR"] 6 | 7 | INDEX_PAGE=FOOBAR 8 | URI_PROTOCOL=FOOBAR 9 | WEBHOST=FOOBAR 10 | SUBDIRECTORY=FOOBAR 11 | SITENAME=FOOBAR 12 | HIDE_INACTIVE_MODULES=TRUE 13 | LOCALADMIN_THRESHOLD=99 14 | AUTH_METHODS=FOOBAR 15 | RECAPTCHA_LOGIN_PUBLIC_KEY=FOOBAR 16 | RECAPTCHA_LOGIN_PRIVATE_KEY=FOOBAR 17 | AUTHORIZATION_DELETE_MACHINE=FOO,BAR 18 | AUTHORIZATION_GLOBAL=FOO,BAR 19 | ROLES_ADMIN=FOO,BAR 20 | GROUPS_ADMIN_USERS=FOO,BAR 21 | ENABLE_BUSINESS_UNITS=TRUE 22 | AUTH_SECURE=TRUE 23 | VNC_LINK=FOOBAR 24 | SSH_LINK=FOOBAR 25 | BUNDLEID_IGNORELIST=FOO,BAR 26 | BUNDLEPATH_IGNORELIST=FOO,BAR 27 | 28 | GSX_ENABLE=TRUE 29 | GSX_CERT=FOOBAR 30 | GSX_CERT_KEYPASS=FOOBAR 31 | GSX_SOLD_TO=FOOBAR 32 | GSX_SHIP_TO=FOOBAR 33 | GSX_USERNAME=FOOBAR 34 | GSX_DATE_FORMAT=FOOBAR 35 | 36 | GOOGLE_MAPS_API_KEY=FOOBAR 37 | CURL_CMD=FOO,BAR 38 | MWA2_LINK=FOOBAR 39 | MODULES=FOO,BAR 40 | DISPLAYS_INFO_KEEP_PREVIOUS=TRUE 41 | TEMPERATURE_UNIT=FOOBAR 42 | CLIENT_PASSPHRASES=FOO,BAR 43 | PREFLIGHT_SCRIPT=FOOBAR 44 | POSTFLIGHT_SCRIPT=FOOBAR 45 | REPORT_BROKEN_CLIENT_SCRIPT=FOOBAR 46 | GUZZLE_HANDLER=FOOBAR 47 | REQUEST_TIMEOUT=99 48 | APPLE_HARDWARE_ICON_URL=FOOBAR 49 | APPS_TO_TRACK=FOO,BAR 50 | DISK_REPORT_THRESHOLD_DANGER=99 51 | DISK_REPORT_THRESHOLD_WARNING=99 52 | 53 | AUTH_AD_ACCOUNT_SUFFIX=FOOBAR 54 | AUTH_AD_BASE_DN=FOOBAR 55 | AUTH_AD_DOMAIN_CONTROLLERS=FOO,BAR 56 | AUTH_AD_ADMIN_USERNAME=FOOBAR 57 | AUTH_AD_ADMIN_PASSWORD=FOOBAR 58 | AUTH_AD_ALLOWED_USERS=FOO,BAR 59 | AUTH_AD_ALLOWED_GROUPS=FOO,BAR 60 | AUTH_AD_RECURSIVE_GROUPSEARCH=TRUE 61 | 62 | AUTH_LDAP_SERVER=FOOBAR 63 | AUTH_LDAP_USER_BASE=FOOBAR 64 | AUTH_LDAP_GROUP_BASE=FOOBAR 65 | AUTH_LDAP_ALLOWED_USERS=FOO,BAR 66 | AUTH_LDAP_ALLOWED_GROUPS=FOO,BAR 67 | AUTH_LDAP_USER_FILTER=FOOBAR 68 | AUTH_LDAP_GROUP_FILTER=FOOBAR 69 | AUTH_LDAP_PORT=99 70 | AUTH_LDAP_VERSION=99 71 | AUTH_LDAP_USE_STARTTLS=TRUE 72 | AUTH_LDAP_FOLLOW_REFERRALS=TRUE 73 | AUTH_LDAP_DEREF=99 74 | AUTH_LDAP_BIND_DN=FOOBAR 75 | AUTH_LDAP_BIND_PASSWORD=FOOBAR 76 | AUTH_LDAP_USER_SCOPE=FOOBAR 77 | AUTH_LDAP_GROUP_SCOPE=FOOBAR 78 | AUTH_LDAP_GROUP_KEY=FOOBAR 79 | AUTH_LDAP_DEBUG=TRUE 80 | 81 | AUTH_SAML_SP_NAME_ID_FORMAT=FOOBAR 82 | AUTH_SAML_SP_ENTITY_ID=FOOBAR 83 | AUTH_SAML_IDP_ENTITY_ID=FOOBAR 84 | AUTH_SAML_IDP_SSO_URL=FOOBAR 85 | AUTH_SAML_IDP_SLO_URL=FOOBAR 86 | AUTH_SAML_IDP_X509CERT=FOOBAR 87 | AUTH_SAML_USER_ATTR=FOO 88 | AUTH_SAML_GROUP_ATTR=BAR 89 | AUTH_SAML_ALLOWED_USERS=FOO,BAR 90 | AUTH_SAML_ALLOWED_GROUPS=FOO,BAR 91 | AUTH_SAML_DISABLE_SSO=TRUE 92 | AUTH_SAML_DEBUG=TRUE 93 | AUTH_SAML_SECURITY_NAME_ID_ENCRYPTED=TRUE 94 | AUTH_SAML_SECURITY_AUTHN_REQUESTS_SIGNED=TRUE 95 | AUTH_SAML_SECURITY_LOGOUT_REQUEST_SIGNED=TRUE 96 | AUTH_SAML_SECURITY_LOGOUT_RESPONSE_SIGNED=TRUE 97 | AUTH_SAML_SECURITY_SIGN_METADATA=TRUE 98 | AUTH_SAML_SECURITY_WANT_MESSAGES_SIGNED=TRUE 99 | AUTH_SAML_SECURITY_WANT_ASSERTIONS_ENCRYPTED=TRUE 100 | AUTH_SAML_SECURITY_WANT_ASSERTIONS_SIGNED=TRUE 101 | AUTH_SAML_SECURITY_WANT_NAME_ID=TRUE 102 | AUTH_SAML_SECURITY_WANT_NAME_ID_ENCRYPTED=TRUE 103 | AUTH_SAML_SECURITY_REQUESTED_AUTHN_CONTEXT=TRUE 104 | AUTH_SAML_SECURITY_WANT_XML_VALIDATION=TRUE 105 | AUTH_SAML_SECURITY_RELAX_DESTINATION_VALIDATION=TRUE 106 | AUTH_SAML_SECURITY_SIGNATURE_ALGORITHM=FOOBAR 107 | AUTH_SAML_SECURITY_DIGEST_ALGORITHM=FOOBAR 108 | AUTH_SAML_SECURITY_LOWERCASE_URLENCODING=TRUE 109 | 110 | -------------------------------------------------------------------------------- /app/lib/munkireport/Dashboard.php: -------------------------------------------------------------------------------- 1 | config = $config; 16 | $this->dashboards['default'] = [ 17 | 'name' => 'default', 18 | 'dashboard_layout' => $config['default_layout'], 19 | 'display_name' => 'Dashboard', 20 | 'hotkey' => 'd', 21 | ]; 22 | 23 | if($loadAll){ 24 | $this->fileSystem = new Filesystem; 25 | $this->loadAll(); 26 | } 27 | } 28 | 29 | private function dashboardExists($dashboard) 30 | { 31 | return isset($this->dashboards[$dashboard]); 32 | } 33 | 34 | private function getDashboardLayout($dashboard) 35 | { 36 | return $this->dashboards[$dashboard]['dashboard_layout']; 37 | } 38 | 39 | private function addDashboard($path) 40 | { 41 | try { 42 | $filename = $this->fileSystem->name($path); 43 | $display_name = $filename; 44 | $hotkey = ''; 45 | $data = Yaml::parseFile($path); 46 | if(isset($data['display_name'])) 47 | { 48 | $display_name = $data['display_name']; 49 | unset($data['display_name']); 50 | } 51 | if(isset($data['hotkey'])) 52 | { 53 | $hotkey = $data['hotkey']; 54 | unset($data['hotkey']); 55 | } 56 | $this->dashboards[$filename] = [ 57 | 'name' => $filename, 58 | 'display_name' => $display_name, 59 | 'hotkey' => $hotkey, 60 | 'dashboard_layout' => $data, 61 | ]; 62 | } catch (\Exception $e) { 63 | // Do something 64 | } 65 | } 66 | 67 | public function loadAll() 68 | { 69 | if(! $this->loaded){ 70 | foreach($this->config['search_paths'] as $dir) 71 | { 72 | foreach($this->fileSystem->glob($dir . '/*.yml') AS $file) 73 | { 74 | $this->addDashboard($file); 75 | } 76 | } 77 | $this->loaded = true; 78 | } 79 | return $this; 80 | } 81 | 82 | public function getCount() 83 | { 84 | return count($this->dashboards); 85 | } 86 | 87 | public function getDropdownData($baseUrl, $page) 88 | { 89 | $out = []; 90 | foreach( $this->dashboards as $path => $data){ 91 | $out[] = (object) [ 92 | 'url' => url($baseUrl.'/'.$data['name']), 93 | 'name' => $data['name'], 94 | 'display_name' => $data['display_name'], 95 | 'hotkey' => $data['hotkey'], 96 | 'class' => $page == $baseUrl.'/'.$data['name'] ? 'active' : '', 97 | ]; 98 | } 99 | 100 | return $out; 101 | } 102 | 103 | public function render($dashboard) 104 | { 105 | $view = $this->config['template']; 106 | 107 | if($this->dashboardExists($dashboard)) 108 | { 109 | $data['dashboard_layout'] = $this->getDashboardLayout($dashboard); 110 | } 111 | else 112 | { 113 | $data = ['status_code' => 404]; 114 | $view = 'error/client_error'; 115 | } 116 | $data['widget'] = new Widgets(conf('widget')); 117 | view($view, $data); 118 | 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /app/lib/munkireport/Unserializer.php: -------------------------------------------------------------------------------- 1 | position = 0; 20 | $this->str = $s; 21 | } 22 | 23 | public function await($symbol, $n = 1) 24 | { 25 | #result = $this->take(len(symbol)) 26 | $result = substr($this->str, $this->position, $n); 27 | $this->position += $n; 28 | if ($result != $symbol) { 29 | throw new \Exception(sprintf('Next is `%s` not `%s`', $result, $symbol), 1); 30 | } 31 | } 32 | 33 | public function take($n = 1) 34 | { 35 | $result = substr($this->str, $this->position, $n); 36 | $this->position += $n; 37 | return $result; 38 | } 39 | 40 | public function take_while_not($stopsymbol, $typecast = '') 41 | { 42 | 43 | $stopsymbol_position = strpos($this->str, $stopsymbol, $this->position); 44 | if ($stopsymbol_position === false) { 45 | throw new \Exception(sprintf('No `%s`', $stopsymbol), 1); 46 | } 47 | $result = substr($this->str, $this->position, $stopsymbol_position - $this->position); 48 | 49 | $this->position = $stopsymbol_position + 1; 50 | if ($typecast) { 51 | settype($result, $typecast); 52 | } 53 | return $result; 54 | } 55 | 56 | public function get_rest() 57 | { 58 | return substr($this->str, $this->position); 59 | } 60 | 61 | public function unserialize() 62 | { 63 | 64 | $t = $this->take(); 65 | 66 | 67 | if ($t == 'N') { 68 | $this->await(';'); 69 | return null; 70 | } 71 | 72 | $this->await(':'); 73 | 74 | switch ($t) { 75 | case 'i': 76 | return $this->take_while_not(';', 'int'); 77 | 78 | case 'd': 79 | return $this->take_while_not(';', 'float'); 80 | 81 | case 'b': 82 | return (bool) $this->take_while_not(';', 'int'); 83 | 84 | case 's': 85 | $size = $this->take_while_not(':', 'int'); 86 | $this->await('"'); 87 | $result = $this->take($size); 88 | $this->await('";', 2); 89 | return $result; 90 | 91 | case 'a': 92 | $size = $this->take_while_not(':', 'int'); 93 | return $this->parse_hash_core($size); 94 | 95 | case 'O': 96 | // No object conversion 97 | throw new \Exception("No object conversion allowed", 1); 98 | 99 | default: 100 | throw new \Exception(sprintf('Unknown type `%s`', $t), 1); 101 | } 102 | } 103 | 104 | public function parse_hash_core($size) 105 | { 106 | $result = array(); 107 | $this->await('{'); 108 | $is_array = true; 109 | for ($i=0; $i < $size; $i++) { 110 | $k = $this->unserialize(); 111 | $v = $this->unserialize(); 112 | $result[$k] = $v; 113 | if ($is_array && $k !== $i) { 114 | $is_array = false; 115 | } 116 | } 117 | if ($is_array) { 118 | $result = array_values($result); 119 | } 120 | $this->await('}'); 121 | return $result; 122 | } 123 | } 124 | --------------------------------------------------------------------------------