diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 88efb6a..0000000 --- a/.editorconfig +++ /dev/null @@ -1,20 +0,0 @@ -# Unix-style newlines with a newline ending every file -[*] -indent_style = space -indent_size = 4 -end_of_line = lf -trim_trailing_whitespace = true -charset = utf-8 - -# 2 spaces for JS, HTML and CSS/SASS -[*.{ts,js,html,yml,css,scss,sass,less}] -indent_size = 2 - -# 4 spaces for markdown -[*.md] -indent_size = 4 -trim_trailing_whitespace = false - -# Hard TAB for Makefile -[Makefile] -indent_style = tab diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index a0785e3..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Build - -on: - push: - branches: [master] - pull_request: - branches: [master] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Use Node.js 23.x - uses: actions/setup-node@v4 - with: - node-version: 23.x - cache: npm - - - name: Build Firefox - run: make build-firefox - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: firefox - path: artifacts/firefox.zip - - name: Check reproducibility from src-zip - run: make test-reproducibility-firefox - - - name: Build Chrome - run: make build-chrome - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: chrome - path: artifacts/chrome.zip - - name: Check reproducibility from src-zip - run: make test-reproducibility-chrome - - typecheck: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Use Node.js 23.x - uses: actions/setup-node@v4 - with: - node-version: 23.x - cache: npm - - - name: Install dependencies - run: make install - - - name: Typecheck TypeScript - run: make compile diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 707d50e..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: CodeQL - -on: - push: - branches: [master] - pull_request: - branches: [master] - schedule: - - cron: 25 4 * * 3 - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [javascript] - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - queries: security-and-quality - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: /language:${{ matrix.language }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 0e817e9..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Lint - -on: - push: - branches: [master] - pull_request: - branches: [master] - -jobs: - lint: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Use Node.js 23.x - uses: actions/setup-node@v4 - with: - node-version: 23.x - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Run Prettier - run: npx prettier --check . diff --git a/.gitmodules b/.gitmodules index d4b7a1b..0fc3a54 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "media"] path = media - url = https://github.com/ActivityWatch/media.git + url = https://github.com/haroon7v/aw-media diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index c084c07..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,13 +0,0 @@ -repos: - - repo: local - hooks: - - id: make-format - name: Format code - entry: make format - language: system - pass_filenames: false - - id: make-compile - name: TypeScript compile check - entry: make compile - language: system - pass_filenames: false diff --git a/src/background/blocker.ts b/src/background/blocker.ts new file mode 100644 index 0000000..73b6b50 --- /dev/null +++ b/src/background/blocker.ts @@ -0,0 +1,60 @@ +import browser from 'webextension-polyfill' +import config from '../config' +import { assetsonarServerUrl } from './helpers' + + +export const blockedDomainsAlarmListener = () => async (alarm: browser.Alarms.Alarm) => { + if (alarm.name !== config.blockedDomains.alarmName) return + + const response = await fetchBlockedDomains() + // setDomains(response.domains) discuss if required + await updateDynamicRules(response.domains) +} + +export const updateDynamicRules = async (domains: Array<{ id: string, domain: string, match_type: string }>) => { + const existingRules = await browser.declarativeNetRequest.getDynamicRules() + const domainIds = new Set(domains.map(domain => domain.id)) + const removeRuleIds = existingRules + .map(rule => rule.id) + .filter(id => !domainIds.has(id.toString())) + + const rules = domains.map(domain => ({ + id: Number(domain.id), + priority: 1, + action: { type: "block" }, + condition: { + regexFilter: buildUrlFilter(domain.domain, domain.match_type), + resourceTypes: ["main_frame"] + } + } as browser.DeclarativeNetRequest.Rule)) + if (rules.length > 0) { + await browser.declarativeNetRequest.updateDynamicRules({ addRules: rules, removeRuleIds }) + } +} + +function buildUrlFilter(domain: string, matchType: string): string { + switch (matchType) { + case 'starts_with': + return `^(https?://)?(www\\.)?${domain}(\\.[^/?#]+)*([/?#]|$)` + case 'ends_with': + return `^(https?://)?(www\\.)?[^/?#]*${domain}([/?#]|$)` + default: + return `^(https?://)?(www\\.)?${domain}([/?#]|$)` + } +} + +const fetchBlockedDomains = async () => { + // Read subdomain and itam_access_token from managed storage + const managed = await browser.storage.managed.get(['subdomain', 'itam_access_token']) + const subdomain = typeof managed.subdomain === 'string' ? managed.subdomain : 'comtest' + const itamAccessToken = typeof managed.itam_access_token === 'string' ? managed.itam_access_token : '4c7e2fc19aa3ca6abdd905f7d99dd574' + if (!subdomain) throw new Error('subdomain not found in managed storage') + if (!itamAccessToken) throw new Error('itam_access_token not found in managed storage') + + const url = `${assetsonarServerUrl(subdomain)}/api/api_integration/blocked_web_domains.api?itam_access_token=${encodeURIComponent(itamAccessToken)}` + const response = await fetch(url) + if (!response.ok) { + throw new Error(`Failed to fetch blocked domains: ${response.statusText}`) + } + return response.json() +} diff --git a/src/background/helpers.ts b/src/background/helpers.ts index c6f46c2..1c0aa8b 100644 --- a/src/background/helpers.ts +++ b/src/background/helpers.ts @@ -1,4 +1,5 @@ import browser from 'webextension-polyfill' +import config from '../config' import { FetchError } from 'aw-client' import { getBrowserName, setBrowserName } from '../storage' @@ -88,3 +89,8 @@ export async function logHttpError(error: T) { console.error('Unexpected error', error) } } + +export const assetsonarServerUrl = (subdomain?: string) => { + const { protocol, host } = config.assetsonarServer + return `${protocol}://${subdomain}.${host}` +} diff --git a/src/background/main.ts b/src/background/main.ts index a1b092b..be92e39 100644 --- a/src/background/main.ts +++ b/src/background/main.ts @@ -5,6 +5,7 @@ import { sendInitialHeartbeat, tabActivatedListener, } from './heartbeat' +import { blockedDomainsAlarmListener } from './blocker' import { getClient, detectHostname } from './client' import { getConsentStatus, @@ -64,7 +65,11 @@ console.debug('Creating alarms and tab listeners') browser.alarms.create(config.heartbeat.alarmName, { periodInMinutes: Math.floor(config.heartbeat.intervalInSeconds / 60), }) +browser.alarms.create(config.blockedDomains.alarmName, { + periodInMinutes: Math.floor(config.blockedDomains.intervalInSeconds / 60) +}) browser.alarms.onAlarm.addListener(heartbeatAlarmListener(client)) +browser.alarms.onAlarm.addListener(blockedDomainsAlarmListener()) browser.tabs.onActivated.addListener(tabActivatedListener(client)) console.debug('Setting base url') diff --git a/src/config.ts b/src/config.ts index d89b0b0..f74c53f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,10 +1,18 @@ const config = { isDevelopment: import.meta.env.DEV, requireConsent: import.meta.env.VITE_TARGET_BROWSER === 'firefox', + assetsonarServer: { + protocol: import.meta.env.DEV ? 'http' : 'https', + host: import.meta.env.DEV ? 'lvh.me:3000' : 'assetsonar.com', + }, heartbeat: { alarmName: 'heartbeat', intervalInSeconds: 60, }, + blockedDomains: { + alarmName: 'blockedDomains', + intervalInSeconds: 1800, + }, } export default config diff --git a/src/manifest.json b/src/manifest.json index 41dfda8..0893a9e 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -2,9 +2,9 @@ "{{chrome}}.manifest_version": 3, "{{firefox}}.manifest_version": 2, - "name": "ActivityWatch Web Watcher", - "description": "Log the current tab and your browser activity with ActivityWatch.", - "version": "0.5.3", + "name": "AssetSonar SaaS Discovery & Usage Monitor", + "description": "Gain visibility into SaaS usage, track shadow IT, and optimize software management with AssetSonar's Chrome Extension.", + "version": "1.0", "icons": { "128": "logo-128.png" }, @@ -42,7 +42,8 @@ "alarms", "notifications", "activeTab", - "storage" + "storage", + "declarativeNetRequest" ], "{{chrome}}.host_permissions": [""], diff --git a/src/storage.ts b/src/storage.ts index 60cfdd3..0ec322f 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -101,3 +101,20 @@ export const getHostname = (): Promise => .then((data: StorageData) => data.hostname as string | undefined) export const setHostname = (hostname: Hostname) => browser.storage.local.set({ hostname }) + +type Domain = { id: string, domain: string, matchType: string } +export const getDomains = (): Promise => + browser.storage.local + .get('domains') + .then((data: StorageData) => { + const domains = data.domains as Domain[] | undefined + if (!Array.isArray(domains)) return undefined + return domains.map(domain => ({ + id: domain.id as string, + domain: domain.domain as string, + matchType: domain.matchType as string + })) + }) + +export const setDomains = (domains: Domain[]): Promise => + browser.storage.local.set({ domains })