Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/release-current-version.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name: Release Current Version

on:
push:
branches: [ master, main, support/v1 ]

jobs:
Run:
uses: polarityio/polarity-github-actions/.github/workflows/release-integration.yml@master
24 changes: 24 additions & 0 deletions .github/workflows/run-int-dev-checklist.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Run Integration Development Checklist

on:
pull_request:
branches: [ master, main, develop, support/v1 ]

jobs:
run-integration-development-checklist:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
- name: Test NPM Install
id: test-npm-install
run: |
npm ci
- name: Polarity Integration Development Checklist
id: int-dev-checklist
uses: polarityio/polarity-integration-development-checklist@main
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41 changes: 41 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Ignore Maven Compiled Project Files
*/target/

# Ignore IntelliJ Project Information
*.iml
.idea/

# Ignore Sublime Text Project Files
*.sublime-project
*.sublime-workspace

# Ignore Eclipse Project Information
*.settings
*.project
*.classpath
dependency-reduced-pom.xml

# Ignore Excel Files
*.xlsx

# Ignore local log files
*.log

# Ignore Generated HTML README Files
README.html

# Ignored installed NPM modules
node_modules/

# Ignore private keys
key/

# Ignore VSCode
*.history
.histoy
*.vscode
.vscode

# Others
legacy_wrapper.js
dist/
88 changes: 87 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,87 @@
# UniFi Network Polarity Integration
# Polarity UniFi Network Integration

Polarity's UniFi Network integration queries your on-premise UniFi Network controller for **IPv4 addresses** and **MAC addresses**, returning real-time information about connected clients and infrastructure devices (access points, switches, gateways) across **all sites** in your controller.

From the overlay, analysts can **block** or **reconnect** suspicious clients without leaving their workflow.

## Supported Entity Types

| Entity Type | Description |
|---|---|
| `IPv4` | Looks up client connections and infrastructure devices by IP address |
| `MAC Address` | Looks up client connections and infrastructure devices by MAC address |

## Features

| Feature | Detail |
|---|---|
| **Multi-site** | Automatically enumerates all sites in your controller on first lookup; results are shown per site |
| **Client lookup** | Shows hostname, IP, MAC, type (WIRED/WIRELESS/VPN), status (CONNECTED/BLOCKED), SSID, uptime, traffic stats, signal strength |
| **Device lookup** | Shows name, model, product line, firmware version, state (ONLINE/OFFLINE/UPDATING) for APs, switches, and gateways |
| **Block / Reconnect** | One-click block or reconnect of suspicious clients directly from the Polarity overlay |
| **Site cache** | Site list is cached for 1 hour to minimize API calls |

## UniFi Controller Setup

### Supported Controller Versions

This integration supports on-premise UniFi Network controllers running version **10.3.58 or later** with the new API (`/proxy/network/integration`).

### Generating an API Key

1. Log in to your UniFi Network controller
2. Navigate to **Settings → Admins & Users → API**
3. Click **Create API Key** and copy the key
4. The API Key needs at minimum: **read access** to sites, clients, and devices; **write access** to execute client actions (block/reconnect)

### Controller URL Format

The URL must point to the integration API base path:

```
https://{your-controller-host}/proxy/network/integration
```

**Examples:**
- `https://192.168.1.1/proxy/network/integration`
- `https://unifi.company.com/proxy/network/integration`

> ⚠️ The URL must **not** end with a trailing slash.

## Integration Options

| Option | Description | Admin Only |
|---|---|---|
| **UniFi Controller URL** | Base URL of your on-premise UniFi controller (no trailing slash) | ✅ |
| **API Key** | Your UniFi Network API Key | ✅ |
| **Ignored Entities** | Comma-separated list of IPs or MACs to skip | ❌ |
| **IP Blocklist Regex** | Regular expression for IPs to exclude from lookup | ❌ |

## Block / Reconnect Actions

When a **connected client** is found in an overlay result:

- **⛔ Block Client** — sends a `block` action to the UniFi controller, preventing the client from accessing the network. The status updates to `BLOCKED` immediately in the overlay.
- **🔄 Reconnect** — sends a `reconnect` action to restore the client's network access. The status updates to `CONNECTED` immediately in the overlay.

> Infrastructure devices (access points, switches, gateways) do **not** have action buttons in V1 — they are read-only.

## Multi-Site Behavior

On the first lookup after startup (or after the 1-hour cache expires), the integration fetches the full list of sites from `GET /v1/sites`. All subsequent lookups fan out across every site in parallel, running both client and device queries per site.

Results are grouped by site in the overlay:
- Each client match appears in a **🖥 Connected Client — {site name}** collapsible section
- Each device match appears in a **📡 Infrastructure Device — {site name}** collapsible section

## Installation

1. Clone or download this integration into your Polarity integrations directory
2. Run `npm install`
3. Configure the **UniFi Controller URL** and **API Key** in Polarity's integration settings

## About Polarity

Polarity is a memory-augmentation platform that automatically overlays relevant contextual information onto any system—browser, terminal, email client, and more—so analysts can act on intelligence without switching tools.

[https://polarity.io](https://polarity.io)
109 changes: 109 additions & 0 deletions components/block.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
polarity.export = PolarityComponent.extend({
details: Ember.computed.alias('block.data.details'),

// ── Computed guards for {{#each}} ────────────────────────────────────────

hasClients: Ember.computed('details.clients', function () {
const clients = this.get('details.clients');
return Array.isArray(clients) && clients.length > 0;
}),

hasDevices: Ember.computed('details.devices', function () {
const devices = this.get('details.devices');
return Array.isArray(devices) && devices.length > 0;
}),

// ── Lifecycle ────────────────────────────────────────────────────────────

init() {
this._super(...arguments);
// Guard ensures idempotent re-renders don't reset state
if (!this.get('block._state')) {
this.set('block._state', {
showClients: true,
showDevices: true,
// Per-client loading flags keyed by clientId
isBlocking: {},
// Per-client inline feedback messages keyed by clientId
actionMessage: {}
});
}
},

// ── Actions ──────────────────────────────────────────────────────────────

actions: {
toggleSection(section) {
const key = `block._state.show${section}`;
this.set(key, !this.get(key));
},

/**
* Send a BLOCK_CLIENT message for the given client result.
* @param {Object} client - a member of details.clients[]
*/
blockClient(client) {
const clientId = client.clientId;
const busyKey = `block._state.isBlocking.${clientId}`;
const msgKey = `block._state.actionMessage.${clientId}`;

this.set(busyKey, true);
this.set(msgKey, '');

this.sendIntegrationMessage({
action: 'BLOCK_CLIENT',
siteId: client.siteId,
clientId
})
.then((response) => {
if (response && response.success) {
Ember.set(client, 'status', 'BLOCKED');
this.set(msgKey, '✅ Client blocked');
} else {
const msg = (response && response.message) || 'Unknown error';
this.set(msgKey, `⚠️ Failed: ${msg}`);
}
})
.catch((err) => {
this.set(msgKey, `⚠️ Error: ${err.message || err}`);
})
.finally(() => {
this.set(busyKey, false);
});
},

/**
* Send a RECONNECT_CLIENT message for the given client result.
* @param {Object} client - a member of details.clients[]
*/
reconnectClient(client) {
const clientId = client.clientId;
const busyKey = `block._state.isBlocking.${clientId}`;
const msgKey = `block._state.actionMessage.${clientId}`;

this.set(busyKey, true);
this.set(msgKey, '');

this.sendIntegrationMessage({
action: 'RECONNECT_CLIENT',
siteId: client.siteId,
clientId
})
.then((response) => {
if (response && response.success) {
Ember.set(client, 'status', 'CONNECTED');
this.set(msgKey, '✅ Client reconnected');
} else {
const msg = (response && response.message) || 'Unknown error';
this.set(msgKey, `⚠️ Failed: ${msg}`);
}
})
.catch((err) => {
this.set(msgKey, `⚠️ Error: ${err.message || err}`);
})
.finally(() => {
this.set(busyKey, false);
});
}
}
});
3 changes: 3 additions & 0 deletions components/summary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
polarity.export = PolarityComponent.extend({
details: Ember.computed.alias('block.data.details')
});
72 changes: 72 additions & 0 deletions config/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use strict';

module.exports = {
name: 'UniFi Network',
acronym: 'UNF',
description:
'Query IP addresses and MAC addresses across all sites on an on-premise UniFi Network controller. Supports block and reconnect actions for connected clients.',
entityTypes: ['IPv4', 'MAC'],
defaultColor: 'light-blue',
styles: ['./styles/styles.less'],
onDemandOnly: false,
block: {
component: { file: './components/block.js' },
template: { file: './templates/block.hbs' }
},
// UniFi controllers commonly use self-signed TLS certificates.
// rejectUnauthorized is set to false to allow connections to controllers
// that have not been issued a publicly trusted certificate.
request: {
cert: '',
key: '',
passphrase: '',
ca: '',
proxy: '',
rejectUnauthorized: false
},
logging: {
level: 'info'
},
options: [
{
key: 'url',
name: 'UniFi Controller URL',
description:
'The base URL of your on-premise UniFi Network controller. Must NOT end with a trailing slash. Example: https://192.168.1.1/proxy/network/integration',
default: '',
type: 'text',
userCanEdit: false,
adminOnly: true
},
{
key: 'apiKey',
name: 'API Key',
description:
'Your UniFi Network controller API Key. Generate one from the controller UI under Settings → API.',
default: '',
type: 'password',
userCanEdit: false,
adminOnly: true
},
{
key: 'blocklist',
name: 'Ignored Entities',
description:
'Comma-separated list of IP addresses or MAC addresses to ignore during lookup.',
default: '',
type: 'text',
userCanEdit: false,
adminOnly: false
},
{
key: 'ipBlocklistRegex',
name: 'IP Blocklist Regex',
description:
'IP addresses matching this regex will not be looked up. Leave blank to query all IP addresses.',
default: '',
type: 'text',
userCanEdit: false,
adminOnly: false
}
]
};
Loading
Loading