Is Opus Advisor safe?

Medium risk

Opus Advisor is medium risk. The shipped code builds a completion record from the active tab's URL and title, then POSTs it to api.internal.opuseps.com/v2/lrs/completed_test. The body is drawn from source; testing never reached the login flow.

opusepsv2.55.0Chrome Web Store
45Risk
Who publishes it

Epilogue Systems, Inc. - 1 other listing from the same operator, none carrying a finding

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
opuseps
Declared legal entity
Epilogue Systems, Inc.
Registered address
259 North Radnor Chester Road, Radnor, PA 19087, US
Registered contact
Bill Bellew, CFO

Same store account

1 other listing published from this account, 4k+ users between them, none of them carrying a finding.

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

Active tab URL sent on tutorial completion

The shipped code builds a completion record from the active tab's URL and title, then POSTs it to api.internal.opuseps.com/v2/lrs/completed_test.

The body is drawn from source; testing never reached the login flow.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You close or complete a Follow Me tutorial.

That action reaches the background completion handler after the page script sends the close message.

The extension did this

The extension reads the current active tab and adds its URL and title to the completion record.

The record is then posted to the Opus Advisor API with user and tenant fields from local extension storage.

02EvidenceFIELD TABLE
Fields assembled for the completion POST
FieldValueWhy it matters
Active tab URL
https://intranet.example.com/workflows/quarter-close (illustrative)Shows which page was active in your browser when the tutorial completion record was prepared.
Page title
Quarter Close Workflow - Finance Portal (illustrative)Adds readable page context next to the URL, which can reveal what you were viewing.
User name
alex.lee@example.com (illustrative)Links the completion record to the signed-in extension user.
User account ID
48291 (illustrative)Links the record to an internal account identifier for the extension service.
Tenant name and ID
Acme Operations / 1173 (illustrative)Associates the completion event with your organization or workspace in the extension service.
Tutorial document
Invoice Approval Walkthrough / 9247 (illustrative)Shows which tutorial was completed alongside the page that was active at the time.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://api.internal.opuseps.com/v2/lrs/completed_test
The shipped fetch helper returns response.json(); the browser test did not capture this completion request.
Headers
sourceweb-recorder
content-typeapplication/json
authorizationextension auth token from chrome.storage.sync
04EvidenceCODE COMPARE
The code that does this

The completion path reads the active tab and posts it to the API

What it actually does
Readable equivalent of the content-script completion signalcontent.js
function closeFollowmeMode() {
  minimizePlayer();
  removeTimelineToggleButton();
  chromeRuntimeSendMessage("close-followme-mode");
}
Readable equivalent of the background completion handlerbackground.js
if (request.msg === "close-followme-mode") {
  (async () => {
    isFMCompletePending = true;
    documentInfo.document.id = playerState.document.id;
    documentInfo.document.title = playerState.document.title;
    await SendCompleteDocumentStatementAsync();
    resetPlayerState();
  })();
}
Readable equivalent of the completion POST body builderbackground.js
async function SendCompleteDocumentStatementAsync() {
  const initTime = new Date();
  const userDetailsResult = await chrome.storage.local.get("current_user_profile");
  const userDetails = userDetailsResult.current_user_profile;
  const docId = playerState.document.id;
  const docName = playerState.document.title;
  const stringifiedPlayerState = JSON.stringify(playerState);

  chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
    let caption = "";
    let url = "";

    if (tabs.length > 0) {
      activeTabId = tabs[0].id;
      caption = tabs[0].title;
      url = tabs[0].url;
      addFMCompleteDialog(activeTabId, stringifiedPlayerState);
    }

    let results = await chrome.storage.local.get("stepsContextMatchingStatus");
    results = results.stepsContextMatchingStatus;
    const totalSteps = results.length;
    const matchedSteps = results.filter((value) => value).length;
    const scaled = matchedSteps / totalSteps;

    const data = {
      document: {
        docType: 8,
        id: docId,
        name: docName,
      },
      result: {
        max: totalSteps,
        min: 0,
        raw: matchedSteps,
        completion: true,
        scaled: scaled,
      },
      userId: userDetails.id,
      userName: userDetails.loginName,
      tenancyId: userDetails.tenantId,
      tenancyName: userDetails.tenant.tenancyName,
      verb: "completed",
      duration: new Date().getTime() - initTime.getTime(),
      initTime: initTime,
      caption: caption,
      url: url,
      contextRegistrationId: "",
    };

    doPostV2("/lrs/completed_test", data);
  });
}
Readable equivalent of the JSON POST helperbackground.js
async function doPostV2(url, body, accessToken = null) {
  const token = accessToken || (await chrome.storage.sync.get("auth_token")).auth_token;

  const response = await fetch(`https://api.internal.opuseps.com/v2${url}`, {
    headers: {
      accept: "application/json, text/plain, */*",
      authorization: token,
      "content-type": "application/json",
      source: "web-recorder",
    },
    body: JSON.stringify(body),
    method: "POST",
    mode: "cors",
    credentials: "include",
  });

  return response.json();
}
05EvidenceTHIRD PARTY LIST
Destination named by the shipped code
  • api.internal.opuseps.com

    Receives the Follow Me tutorial completion POST at /v2/lrs/completed_test, including the active tab URL and title assembled by background.js.

What it can do

Permissions this extension asks for, as declared in version 2.54.0. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to. The current listed version is 2.55.0, which we have not unpacked yet.

  • Read and change your data on every secure site you visit

    https://*/*

  • Read and change your data on every site you visit

    http://*/*

  • Store data in your browser

    storage

  • Act on the current tab, but only after you click the extension

    activeTab

  • Run its own code inside the pages you visit

    scripting

  • See the address and title of every tab you have open

    tabs

  • See, disable and uninstall your other extensions, including your security ones

    management

microphoneaudioCapture
Updated 30 September 2026kgmohllpiohdkkhhkfneojbadinddkpm