Synchronize vulnerability count across all data sources and network intelligence
()
| 3130 | # card, and counting it here would contradict that. |
| 3131 | 'degraded': sum(1 for p in ragnar_peers if p['online'] |
| 3132 | and p.get('health') is not None |
| 3133 | and not p['health'].get('reachable')), |
| 3134 | 'duplicate_unit_ids': duplicates, |
| 3135 | 'duplicate_names': duplicate_names, |
| 3136 | 'unnumbered_units': unnumbered, |
| 3137 | # Mesh-wide health, rolled up server-side so a fleet of thousands is |
| 3138 | # one small object here instead of the browser crunching every node. |
| 3139 | 'health': health_rollup, |
| 3140 | }, |
| 3141 | }) |
| 3142 | |
| 3143 | |
| 3144 | @app.route('/api/mesh/control', methods=['POST']) |
| 3145 | def mesh_control(): |
| 3146 | """Start/stop one of this unit's monitors — the mesh's write endpoint. |
| 3147 | |
| 3148 | Reachable two ways, both trusted: |
| 3149 | * a tagged peer Ragnar over the tailnet (the auth bypass in |
| 3150 | check_authentication lets THIS one POST through on WireGuard identity); |
| 3151 | * the local operator with a session (self-control, no network hop). |
| 3152 | |
| 3153 | Everything actuable is allowlisted in `_mesh_apply_control`; there is no way |
| 3154 | to reach an arbitrary route from here. |
| 3155 | """ |
| 3156 | if not mesh_available: |
| 3157 | return jsonify({'success': False, 'error': 'mesh_manager unavailable'}), 503 |
| 3158 | data = request.get_json(silent=True) or {} |
| 3159 | result = _mesh_apply_control(data.get('feature'), data.get('action')) |
| 3160 | return jsonify(result), (200 if result.get('success') else 400) |
| 3161 | |
| 3162 | |
| 3163 | @app.route('/api/mesh/peer-control', methods=['POST']) |
| 3164 | def mesh_peer_control(): |
| 3165 | """Operator-facing proxy: tell a *peer* unit to start/stop a monitor. |
| 3166 | |
| 3167 | The browser can't dial a peer's tailnet IP (it isn't on the tailnet — that's |
| 3168 | the whole reason peer data is pulled server-side), so this unit relays the |
| 3169 | command over the tailnet on the operator's behalf, exactly as it relays |
| 3170 | polls. A command aimed at THIS unit is applied directly with no network hop. |
| 3171 | """ |
| 3172 | if not mesh_available: |
| 3173 | return jsonify({'success': False, 'error': 'mesh_manager unavailable'}), 503 |
| 3174 | if not _mesh_enabled(): |
| 3175 | return jsonify({'success': False, 'error': 'Mesh is not enabled on this unit.'}), 400 |
| 3176 | data = request.get_json(silent=True) or {} |
| 3177 | feature = (data.get('feature') or '').strip().lower() |
| 3178 | action = (data.get('action') or '').strip().lower() |
| 3179 | node_id = (data.get('node_id') or '').strip() |
| 3180 | if feature not in _MESH_CONTROLLABLE: |
| 3181 | return jsonify({'success': False, 'error': f'Unknown feature {feature!r}.'}), 400 |
| 3182 | if action not in ('start', 'stop'): |
| 3183 | return jsonify({'success': False, 'error': f'Unknown action {action!r}.'}), 400 |
| 3184 | |
| 3185 | # Everything below can touch Tailscale/the network, so guard it: this route |
| 3186 | # must ALWAYS return JSON. A 500 HTML error page here is what surfaces in the |
| 3187 | # browser as an opaque "SyntaxError: The string did not match the expected |
| 3188 | # pattern" when the client tries to parse it, hiding the real cause. |
| 3189 | try: |
no test coverage detected