Certification, reporting, challenge-period, or LiveReport

Which IIQ version are you inquiring about?

[Replace this text with your version of IIQ. The more specific you can be (7.1, 8.3, 8.X), the more people can help. If you do not know, put Unsure.]
Identity iq 8.4p4

Share all details about your problem, including any error messages you may have received.

[Replace this text with the problem that you are facing]

We are evaluating the Challenge Period functionality for Access Certifications in IIQ 8.4p4.

We are looking for a report that provides the complete history of actions for each certification item. For example:

Certifier Revoke + Comment → Affected User Challenge + Comment → Certifier Accept/Reject Challenge + Comment → Subsequent/Final Certifier Decision + Comment

We tested the Targeted Access Review Live Report. It provides fields such as Decision, Decision Maker, and Decision Maker Comments, but we are unable to find the complete historical sequence, particularly the affected user’s Challenge Period comments and the certifier’s subsequent actions.

Is there an out-of-the-box report in IIQ 8.4p4 that provides this complete history?

If not, is this information available through an IIQ object/API that can be used to develop a custom LiveReport?

Our requirement is to retrieve this information across all certification items in a campaign rather than reviewing each certification item individually.

Hello,

We are using a modified version of the “Certification Activity by Application Live Report”.

The report has been customized for our specific requirements, but I believe you may find some useful information in it. In particular, it includes fields related to the Challenge Period and associated comments.

The code below is not a complete working report. It is only an excerpt taken from our working report, with some parts removed for simplicity. However, it may help you identify the relevant objects, fields, or data sources needed to retrieve the information you are looking for.

<DataSource dataSourceClass="sailpoint.reporting.datasource.CertificationLiveReportDataSource" objectType="CertificationItem" type="Java">
  <QueryParameters>
    <Parameter argument="phase" defaultValue="Staged" operation="NE" property="parent.certification.phase" valueClass="sailpoint.object.Certification$Phase"/>
    <Parameter argument="type">
      <QueryScript>
        <Source>
          import sailpoint.object.\*;
          queryOptions.addFilter(Filter.ne("parent.certification.type", Certification.Type.BusinessRoleComposition));
          queryOptions.addFilter(Filter.ne("parent.certification.type", Certification.Type.AccountGroupPermissions));
          return queryOptions;
        </Source>
      </QueryScript>
    </Parameter>
    <Parameter argument="tags" property="parent.certification.tags.id">
      <QueryScript>
        <Source>
          import sailpoint.object.\*;
          import java.util.\*;
          if (value != null &amp;&amp; !value.isEmpty()){
            queryOptions.addFilter(Filter.containsAll("parent.certification.tags.id", value));
          }
          return queryOptions;
        </Source>
      </QueryScript>
    </Parameter>
    <Parameter argument="certificationGroups" property="parent.certification.certificationGroups.id"/>
    <Parameter argument="applicationNames" property="applicationNames">
      <QueryScript>
        <Source>
          import sailpoint.object.\*;
          import sailpoint.api.ObjectUtil;
          List appNames = new ArrayList();
          if (value != null){
            appNames = ObjectUtil.convertIdsToNames(context, Application.class, value);
          }
          if (appNames.size() != 0){
            queryOptions.addFilter(Filter.containsAll("applicationNames", appNames));
          }
          return queryOptions;
        </Source>
      </QueryScript>
    </Parameter>
    <Parameter argument="creationDate" property="parent.certification.created" valueClass="daterange"/>
    <Parameter argument="signedDate" property="parent.certification.signed" valueClass="daterange"/>
    <Parameter argument="expirationDate" property="parent.certification.expiration" valueClass="daterange"/>
    <Parameter argument="\__guard" defaultValue="1">
      <QueryScript>
        <Source>
          import sailpoint.object.\*;
          import sailpoint.tools.Util;
          import java.util.\*;
          Object tags = arguments.get("tags");
          Object certificationGroups = arguments.get("certificationGroups");
          Object applicationNames = arguments.get("applicationNames");
          Object creationDate = arguments.get("creationDate");
          Object signedDate = arguments.get("signedDate");
          Object expirationDate = arguments.get("expirationDate");
          boolean isEmpty(Object v) {
            if (v == null) return true;
            if (v instanceof String) return Util.isNullOrEmpty((String) v);
            if (v instanceof Collection) return ((Collection) v).isEmpty();
            if (v instanceof Map) {
              Map m = (Map) v;
              Object start = m.get("start");
              Object end   = m.get("end");
              return (start == null &amp;&amp; end == null);
            }
            return false;
          }
          boolean allEmpty =
          isEmpty(tags) &amp;&amp;
          isEmpty(certificationGroups) &amp;&amp;
          isEmpty(applicationNames) &amp;&amp;
          isEmpty(creationDate) &amp;&amp;
          isEmpty(signedDate) &amp;&amp;
          isEmpty(expirationDate);
          if (allEmpty) {
            Calendar cal = Calendar.getInstance();
            cal.add(Calendar.DAY_OF_MONTH, -14);
            Date from = cal.getTime();
            queryOptions.addFilter(Filter.ge("parent.certification.created", from));
          }
          return queryOptions;
        </Source>
      </QueryScript>
    </Parameter>
  </QueryParameters>
</DataSource>
<ReportForm>
  <Reference class="sailpoint.object.Form" id="876543212345676543212341" name="Certification Activity Form"/>
</ReportForm>
<Columns>
  <ReportColumnConfig field="accountDispName" header="Account Name" property="parent.identity" scriptArguments="exceptionEntitlements.nativeIdentity" sortable="true" width="110">
    <RenderScript>
      <Source>
        import sailpoint.object.\*;
        import java.util.\*;
        String result = "";
        String identityName = (String) value;
        String nativeIdentity = (String) scriptArgs.get("exceptionEntitlements.nativeIdentity");
        if (identityName == null || nativeIdentity == null) {
          result = "" + (nativeIdentity != null ? nativeIdentity : ""); // if we do not have identityname but have nativeidentity then return native identity
        } else {   // if we have identityname and native identity (both)
          Identity id = context.getObjectByName(Identity.class, identityName);
          if (id == null) {
            result = "Identity not found: " + identityName;
          } else {
            for (Link link : id.getLinks()) { // take all links and compare to our exceptionEntitlements.nativeIdentity
              if (nativeIdentity.equals(link.getNativeIdentity())) {
                result = link.getDisplayName();
                break;
              }
            }
            if (result.isEmpty()) {
              result = nativeIdentity;
            }
          }
        }
        return result;
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="accountStatus" header="Account Status" property="parent.identity" scriptArguments="exceptionEntitlements.nativeIdentity,exceptionEntitlements.application" sortable="true" width="110">
    <RenderScript>
      <Source>
        import sailpoint.object.\*;
        import java.util.\*;
        String result = "";
        String identityName = (String) value;
        String nativeIdentity = (String) scriptArgs.get("exceptionEntitlements.nativeIdentity");
        String appName = (String) scriptArgs.get("exceptionEntitlements.application");
        if (identityName == null || nativeIdentity == null || appName == null) {
          result = "";  // --  Identity, Application or Account is not available
          return result;
        } else {
          Identity id = context.getObjectByName(Identity.class, identityName);
          if (id != null) {
            for (Link link : id.getLinks()) {
              if (nativeIdentity.equals(link.getNativeIdentity()) &amp;&amp; appName.equals(link.getApplicationName()) ) {
                boolean locked = link.isLocked();
                boolean disabled = link.isDisabled();
                result = (!locked &amp;&amp; !disabled) ? "Active" : (locked ? "Locked" : "") + (locked &amp;&amp; disabled ? " " : "") + (disabled ? "Disabled" : "");
                /*
                String isAccLocked = link.isLocked() ? "locked" : "not locked";
                String isAccDisabled = link.isDisabled() ? "disabled" : "not disabled";
                result = isAccLocked + " " + isAccDisabled;
                */
                break;
              }
            }
            return result.isEmpty() ? "Account not found" : result;
          } else {
            result = "Identity could not be found";
            return result;
          }
        }
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="certificationStart" header="Revalidation Start Timestamp" property="parent.certification.activated" width="110"/>
  <ReportColumnConfig field="certItemFinishDate" header="Certification Item Finish Date" property="id" sortable="true" width="110">
    <RenderScript>
      <Source>
        import sailpoint.object.\*;
        import java.util.\*;
        CertificationItem certItem = context.getObjectById(CertificationItem.class, value);
        return certItem.getFinishedDate();
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="accessReviewFinished" header="Access Review Finish Timestamp" property="parent.certification.finished" width="110"/>
  <ReportColumnConfig field="certificationFinish" header="Revalidation Finish Timestamp" property="parent.certification" scriptArguments="parent.certification.certificationGroups.id, parent.certification.certificationGroups.status" sortable="true" width="110">
    <RenderScript>
      <Source>
        import sailpoint.object.\*;
        import sailpoint.object.Filter;
        import sailpoint.object.QueryOptions;
        import java.util.\*;
        campaignStatus = scriptArgs.get("parent.certification.certificationGroups.status");
        String statusText = (campaignStatus == null) ? null : campaignStatus.toString();
        if ("Complete".equalsIgnoreCase(statusText)) {
          Certification current = (Certification) value;
          if (current == null) {
            return null;
          }
          String groupId = (String) scriptArgs.get("parent.certification.certificationGroups.id");
          QueryOptions qo = new QueryOptions();
          if (groupId != null) {
            qo.addFilter(Filter.eq("certificationGroups.id", groupId));
          } else {
            //qo.addFilter(Filter.eq("id", current.getId()));
            return "parent.certification.certificationGroups.id is NULL";
          }
          qo.addFilter(Filter.notnull("finished"));
          Date latest = null;
          List certs = context.getObjects(Certification.class, qo);
          if (certs != null) {
            for (Object o : certs) {
              Certification c = (Certification) o;
              Date f = c.getFinished();
              if (f != null &amp;&amp; (latest == null || f.after(latest))) {
                latest = f;
              }
            }
          }
          if (latest == null) {
            latest = current.getFinished();
            if (latest == null) { return ""; };
            return "";
          }
          return latest;
        }
        return "";
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="campaignStatus" header="Campaign Status" property="parent.certification.certificationGroups.status" sortable="true" width="110"/>
  <ReportColumnConfig field="nativeIdentity" header="rept_cert_col_account_id" property="exceptionEntitlements.nativeIdentity" sortable="true" width="110"/>
  <ReportColumnConfig field="identityName" header="Account Owner Name" property="parent.identity" scriptArguments="parent.certification.type,targetName" width="110">
    <RenderScript>
      <Source>
        import sailpoint.object.\*;
        Object certType = scriptArgs.get("parent.certification.type");
        Object targetName = scriptArgs.get("targetName");
        if (value == null || Certification.Type.DataOwner.equals(certType)) {
          return targetName;
        }
        Identity myIdentity = context.getObjectByName(Identity.class, String.valueOf(value));
        if (myIdentity == null) {
          return String.valueOf(value);
        }
        String result = myIdentity.getDisplayableName();
        if (result == null || result.trim().length() == 0) {
          return myIdentity.getName();
        }
        return result;
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="identity" header="rept_cert_col_identity" property="parent.identity" scriptArguments="parent.certification.type,targetName" width="110">
    <RenderScript>
      <Source>
        import sailpoint.object.\*;
        if (value == null || Certification.Type.DataOwner.equals(scriptArgs.get("parent.certification.type"))){
          return scriptArgs.get("targetName");
        }
        return value;
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="manager" header="rept_cert_col_manager_name" property="parent" width="110">
    <RenderScript>
      <Source>
        import sailpoint.reporting.\*;
        return value != null ? ReportingLibrary.getCertificationItemManagerName(context, value) : null;
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="signerName" header="Signer Name" property="parent.certification.signOffHistory.signerName" sortable="true" width="110">
    <RenderScript>
      <Source>
        import sailpoint.object.\*;
        result = "";
        if (value != null ){
          Identity myIdentity = context.getObjectByName(Identity.class, value);
          result = myIdentity.getDisplayableName();
        }
        return result;
        // keep original header for field="signer" header="rept_cert_signoff_signer" (next field below)
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="signer" header="Signer WorkerID" property="parent.certification.signOffHistory.signerName" sortable="true" width="110"/>
  <ReportColumnConfig field="signed" header="rept_cert_signoff_signoff_date" property="parent.certification.signOffHistory.date" sortable="true" width="110"/>
  <ReportColumnConfig field="decisionOfSignerBST" header="Decision of Signer" property="id" scriptArguments="parent.certification,parent.certification.signOffHistory.date,parent.certification.signOffHistory.signerName,action.status,action.actorName" width="160">
    <RenderScript>
      <Source>
        import sailpoint.object.\*;
        import java.util.\*;
        String normalizeDecision(Object decision) {
          if (decision == null) { return ""; }
          String s = decision.toString();
          //if ("Remediated".equalsIgnoreCase(s)) {
          //  return "Revoked";
          // }
          // AccountReassign Acknowledged ApproveAccount Approved Cleared Delegated Mitigated Modified Remediated RevokeAccount
          return s;
        }
        String normalizeDecision2(Object decision) {
          if (decision == null) { return ""; }
          String s = decision.toString();
          if ("Remediated".equalsIgnoreCase(s)) {
            return "Revokeded";
          }
          // AccountReassign Acknowledged ApproveAccount Approved Cleared Delegated Mitigated Modified Remediated RevokeAccount
          return s;
        }
        boolean sameDate(Date d1, Date d2) {
          if (d1 == null || d2 == null) {
            return false;
          }
          return d1.getTime() == d2.getTime();
        }
        Date rowSignDate = (Date) scriptArgs.get("parent.certification.signOffHistory.date");
        String rowSigner = (String) scriptArgs.get("parent.certification.signOffHistory.signerName");
        //String finalDecision = normalizeDecision(scriptArgs.get("action.status"));
        finalDecision = scriptArgs.get("action.status");
        String actionActor = (String) scriptArgs.get("action.actorName");
        if (rowSignDate == null || rowSigner == null) {
          return finalDecision;
        }
        Certification certification = (Certification) scriptArgs.get("parent.certification");
        if (certification == null) {
          return finalDecision;
        }
        String certId = certification.getId();
        Map allSignOffData = (Map) renderCache.get("SOH");
        if (allSignOffData == null) {
          allSignOffData = new HashMap();
          renderCache.put("SOH", allSignOffData);
        }
        Map signOffData = (Map) allSignOffData.get(certId);
        if (signOffData == null) { //----------------------------------
          List signOffHistory = certification.getSignOffHistory();
          if (signOffHistory == null || signOffHistory.size() &lt;= 1) {
            signOffData = new HashMap();
            signOffData.put("incomplete", Boolean.TRUE);
            allSignOffData.put(certId, signOffData);
            return finalDecision;
          }
          Date firstSignDate = null;
          Date secondSignDate = null;
          String firstSigner = null;
          String secondSigner = null;
          for (Object o : signOffHistory) {
            if (o == null) {
              continue;
            }
            SignOffHistory soh = (SignOffHistory) o;
            Date signDate = soh.getDate();
            String signerName = soh.getSignerName();
            if (signDate == null || signerName == null) {
              continue;
            }
            if (firstSignDate == null || signDate.before(firstSignDate)) {
              secondSignDate = firstSignDate;
              secondSigner = firstSigner;
              firstSignDate = signDate;
              firstSigner = signerName;
            } else if (secondSignDate == null || signDate.after(secondSignDate)) {
              secondSignDate = signDate;
              secondSigner = signerName;
            }
          }
          if (firstSignDate == null || secondSignDate == null || firstSigner == null || secondSigner == null) {
            signOffData = new HashMap();
            signOffData.put("incomplete", Boolean.TRUE);
            allSignOffData.put(certId, signOffData);
            return finalDecision;
          }
          signOffData = new HashMap();
          signOffData.put("firstSignDate", firstSignDate);
          signOffData.put("secondSignDate", secondSignDate);
          signOffData.put("firstSigner", firstSigner);
          signOffData.put("secondSigner", secondSigner);
          allSignOffData.put(certId, signOffData);
        }
        if (Boolean.TRUE.equals(signOffData.get("incomplete"))) {
          return finalDecision;
        }
        Date firstSignDate = (Date) signOffData.get("firstSignDate");
        Date secondSignDate = (Date) signOffData.get("secondSignDate");
        String firstSigner = (String) signOffData.get("firstSigner");
        String secondSigner = (String) signOffData.get("secondSigner");
        boolean currentRowIsSecondSigner = sameDate(rowSignDate, secondSignDate) || rowSigner.equals(secondSigner);
        if (currentRowIsSecondSigner) {
          return finalDecision;
        }
        if (actionActor == null || actionActor.equals(firstSigner)) {
          return finalDecision;
        }
        if (actionActor.equals(secondSigner)) {
          return "Revoked";
        }
        return finalDecision;
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="esigned" header="rept_cert_signoff_esig" property="parent.certification.signOffHistory.electronicSign" sortable="true" width="110"/>
  <ReportColumnConfig field="certId" header="Access Review ID" property="parent.certification.id" sortable="true" width="110"/>
  <ReportColumnConfig field="certName" header="rept_cert_col_cert_access_review" property="parent.certification.name" sortable="true" width="110"/>
  <ReportColumnConfig field="certGroupName" header="rept_cert_col_cert_grp_name" property="parent.certification.certificationGroups.name" sortable="true" width="110"/>
  <ReportColumnConfig field="tags" header="rept_cert_col_tags" property="parent.certification.tags.name" subQueryKey="id" width="110"/>
  <ReportColumnConfig field="entitlementType" header="rept_cert_col_entitlement_type" property="type" sortable="true" width="110"/>
  <ReportColumnConfig field="entitlement" header="rept_cert_col_entitlement_desc" property="exceptionEntitlements" scriptArguments="bundle,violationSummary,targetName" width="110">
    <RenderScript>
      <Source>
        import sailpoint.reporting.\*;
        return ReportingLibrary.describeCertificationEntitlement(value, scriptArgs.get("bundle"), scriptArgs.get("violationSummary"), scriptArgs.get("targetName"));
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="status" header="rept_cert_col_status" property="summaryStatus" sortable="true" width="110"/>
  <ReportColumnConfig field="decisionFin" header="Final Decision" property="id" scriptArguments="action.status" sortable="true" width="110">
    <RenderScript>
      <Source>
        return scriptArgs.get("action.status");
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="newValue" header="rept_cert_col_newValue" property="action" width="110">
    <RenderScript>
      <Source>
        import sailpoint.reporting.datasource.\*;
        return DataSourceUtil.getRemediationModifiableNewValue(value);
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="remediationCompleted" header="rept_cert_col_revoke_completed" property="action" width="110">
    <RenderScript>
      <Source>
        import sailpoint.reporting.ReportingLibrary;
        import sailpoint.tools.Util;
        import java.lang.String;
        String status = ReportingLibrary.getRemediationStatus(context, value);
        if (status != null &amp;&amp; "yes".equalsIgnoreCase(status.trim())) {
          return "Revoke Completed";
        } else {
          return Util.isNullOrEmpty(status) ? "" : status;
        }
        //return Util.isNullOrEmpty(status) ? "" : status;
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="decisionMakerName" header="Final Decision Maker" property="action.actorDisplayName" sortable="true" width="110"/>
  <ReportColumnConfig field="decisionMaker" header="Final Decision Maker WorkerID" property="action.actorName" sortable="true" width="110"/>
  <ReportColumnConfig field="comments" header="Final Decision Maker Comments" property="action" width="110">
    <RenderScript>
      <Source>
        return value != null ? value.getComments() : null;
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="historyComments" header="Other Comments" property="id" width="110">
    <RenderScript>
      <Source>
        import sailpoint.object.CertificationItem;
        import sailpoint.object.CertificationLink;
        import sailpoint.object.EntitlementSnapshot;
        import sailpoint.object.Filter;
        import sailpoint.object.IdentityHistoryItem;
        import sailpoint.object.QueryOptions;
        import sailpoint.tools.Util;
        String comments = null;
        Iterator iterator = null;
        try {
          CertificationItem certificationItem =
          context.getObjectById(CertificationItem.class, (String) value);
          if (certificationItem.getCertification() == null) {
            return null;
          }
          String certificationId = certificationItem.getCertification().getId();
          String identityName = certificationItem.getIdentity();
          if (identityName == null) {
            return null;
          }
          String applicationName = null;
          String nativeIdentity = null;
          EntitlementSnapshot entitlementSnapshot =
          certificationItem.getExceptionEntitlements();
          if (entitlementSnapshot != null) {
            applicationName = entitlementSnapshot.getApplicationName();
            nativeIdentity = entitlementSnapshot.getNativeIdentity();
          }
          QueryOptions qo = new QueryOptions();
          qo.addFilter(Filter.eq("type", "Comment"));
          qo.addFilter(Filter.eq("identity.name", identityName));
          iterator = context.search(IdentityHistoryItem.class, qo, "id");
          StringBuffer buffer = new StringBuffer();
          while (iterator != null &amp;&amp; iterator.hasNext()) {
            Object\[\] row = (Object\[\]) iterator.next();
            if (row == null || row.length == 0 || row\[0\] == null) {
              continue;
            }
            IdentityHistoryItem historyItem =
            context.getObjectById(IdentityHistoryItem.class, (String) row\[0\]);
            if (historyItem == null) {
              continue;
            }
            CertificationLink certificationLink = historyItem.getCertificationLink();
            if (certificationLink == null ||
            !Util.nullSafeEq(certificationLink.getId(), certificationId)) {
              continue;
            }
            if (applicationName != null &amp;&amp;
            historyItem.getApplication() != null &amp;&amp;
            !Util.nullSafeEq(historyItem.getApplication(), applicationName)) {
              continue;
            }
            if (nativeIdentity != null &amp;&amp;
            historyItem.getNativeIdentity() != null &amp;&amp;
            !Util.nullSafeEq(historyItem.getNativeIdentity(), nativeIdentity)) {
              continue;
            }
            String historyComment = historyItem.getHistoryComments();
            if (historyComment != null) {
              if (buffer.length() > 0) {
                buffer.append(" | ");
              }
              buffer.append(historyComment);
            }
          }
          if (buffer.length() > 0) {
            comments = buffer.toString();
          }
        } finally {
          if (iterator != null) {
            Util.flushIterator(iterator);
          }
        }
        return comments;
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="application" header="rept_certification_form_application" property="exceptionEntitlements.application" sortable="true" width="110"/>
  <ReportColumnConfig field="challengeCrDate" header="Challenge Period Start Date" property="id" width="110">
    <RenderScript>
      <Source>
        import sailpoint.object.\*;
        CertificationItem certItem = context.getObjectById(CertificationItem.class, value);
        CertificationChallenge challenge = certItem.getChallenge();
        if (challenge == null) { return "challenge not created"; }
        //if (!challenge.isChallenged()) { return "isChallenge is null"; }
        challengePerStartDate = challenge.getCreated();
        return new java.text.SimpleDateFormat("dd-MM-yyyy HH:mm:ss").format(challengePerStartDate);
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="challengerComment" header="Challenger Comment" property="id" width="110">
    <RenderScript>
      <Source>
        import sailpoint.object.\*;
        CertificationItem certItem = context.getObjectById(CertificationItem.class, value);
        CertificationChallenge challenge = certItem.getChallenge();
        if (challenge == null || !challenge.isChallenged()) { return null; }
        return challenge.getCompletionComments();
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="certifierDecisionAfterChallengeDate" header="Certifier Decision Date (after challenge)" property="id" width="210">
    <RenderScript>
      <Source>
        import sailpoint.object.\*;
        CertificationItem item = context.getObjectById(CertificationItem.class, value);
        CertificationChallenge ch = item.getChallenge();
        if (ch != null &amp;&amp; ch.isChallenged() &amp;&amp; ch.hasBeenDecidedUpon()) {
          CertificationChallenge.Decision decision = ch.getDecision();
          if (CertificationChallenge.Decision.Accepted.equals(decision)) { // if certifier accepted challange
            CertificationAction action = item.getAction();              // then certItem.action.DecisionDate will change and = certifier last decision date
            if (action != null &amp;&amp; action.getDecisionDate() != null) {
              return new java.text.SimpleDateFormat("dd-MM-yyyy HH:mm:ss").format(action.getDecisionDate());
            }
          }
          // if certifier not accepted challenge then certItem.action.DecisionDate will remain same because certifier did not change decision
          // and to get date of certifiers not accepting decision the only way is to check when challenge was modified last time.
          return new java.text.SimpleDateFormat("dd-MM-yyyy HH:mm:ss").format(ch.getModified());
        }
        //-------------------------------
        if (ch == null &amp;&amp; item.isChallengeGenerated()) {
          return "challenge is null but was generated";
        }
        //-------------------------------
        return null;
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="certifierChallengeDecision" header="Certifier Decision on Challenge" property="id" width="110">
    <RenderScript>
      <Source>
        import sailpoint.object.\*;
        CertificationItem certItem = context.getObjectById(CertificationItem.class, value);
        if (certItem == null) { return null; }
        CertificationChallenge challenge = certItem.getChallenge();
        if (challenge == null) { return null; }
        CertificationChallenge.Decision decision = challenge.getDecision();
        if (decision == null) { return null; }
        return decision.name(); // "Accepted" or "Rejected"
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="certifierCommentOnChallenge" header="Certifier Comment on Challenge" property="id" width="110">
    <RenderScript>
      <Source>
        import sailpoint.object.\*;
        CertificationItem certItem = context.getObjectById(CertificationItem.class, value);
        if (certItem == null) { return null; }
        CertificationChallenge challenge = certItem.getChallenge();
        if (challenge == null) { return null; }
        return challenge.getDecisionComments();
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="exclusionExplanation" header="rept_cert_col_exclusion_explanation" property="parent.certification" width="110">
    <RenderScript>
      <Source>
        if (value != null){
          List archivedEntities = value.fetchArchivedEntities(context);
          if (archivedEntities != null
          &amp;&amp; archivedEntities.size() > 0
          &amp;&amp; archivedEntities.get(0) != null) {
            return archivedEntities.get(0).getExplanation();
          }
        }
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="recommendation" header="rept_cert_col_recommendation" property="recommendValue" scriptArguments="id" sortable="true" width="110">
    <RenderScript>
      <Source>
        import sailpoint.reporting.ReportingLibrary;
        return ReportingLibrary.getRecommendedDecision(context, value, scriptArgs.get("id"), locale, timezone);
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="recommendationReasons" header="rept_cert_col_recommendation_reasons" property="id" width="110">
    <RenderScript>
      <Source>
        import sailpoint.reporting.ReportingLibrary;
        return ReportingLibrary.getRecommendationReasons(context, value, locale, timezone);
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="recommendationTimestamp" header="rept_cert_col_recommendation_timestamp" property="id" width="110">
    <RenderScript>
      <Source>
        import sailpoint.reporting.ReportingLibrary;
        return ReportingLibrary.getRecommendationTimestamp(context, value, locale, timezone);
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="autoDecisionGenerated" header="rept_cert_col_auto_decision_gen" property="id" width="110">
    <RenderScript>
      <Source>
        import sailpoint.reporting.ReportingLibrary;
        return ReportingLibrary.getAutoDecisionGenerated(context, value);
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="autoDecisionAccepted" header="rept_cert_col_auto_decision_acc" property="id" width="110">
    <RenderScript>
      <Source>
        import sailpoint.reporting.ReportingLibrary;
        return ReportingLibrary.getAutoDecisionAccepted(context, value);
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="classifications" header="rept_cert_col_classifications" property="id" width="110">
    <RenderScript>
      <Source>
        import sailpoint.object.CertificationItem;
        import sailpoint.service.classification.ClassificationService;
        import sailpoint.tools.Util;
        CertificationItem item = context.getObjectById(CertificationItem.class, value);
        List names = new ClassificationService(context).getDisplayableNames(item.getClassificationNames());
        return Util.listToCsv(names);
      </Source>
    </RenderScript>
  </ReportColumnConfig>
  <ReportColumnConfig field="elevatedAccess" header="rept_cert_col_elevated_access" property="id" sortable="true" width="110">
    <RenderScript>
      <Source>
        import sailpoint.reporting.ReportingLibrary;
        return ReportingLibrary.isIiqElevatedAccess(context, scriptArgs.get("id"));
      </Source>
    </RenderScript>
  </ReportColumnConfig>
</Columns>
<InitializationRule>
  <Reference class="sailpoint.object.Rule" id="123456789876543745654f" name="Certification Report Customizer"/>
</InitializationRule>


Hi Avtandi,

Thank you for the information, and apologies for the delayed response.

Just to confirm my understanding, the available out-of-the-box reports in IIQ 8.4p4 do not provide the complete Challenge Period history, including the original decision, challenge/comments, and subsequent/final decision.

Does this mean a customized LiveReport would be required to retrieve this history across all certification items in a campaign?

Thanks for your guidance.

@avtan has already provided a great starting point for extracting Challenge Period details. :wink:

One thing I would be careful about is distinguishing the current certification decision from the complete decision history.

While CertificationChallenge can provide the challenge comments and Accept/Reject decision, CertificationItem.getAction() alone may not preserve the original Revoke decision and its comments after a challenge is accepted and the certifier makes a new decision.

I’d suggest inspecting the CertificationItem XML for a test scenario involving Revoke → Challenge → Accept → New Decision, and checking whether the original action is still retained or available through historical/audit records.

Also, for campaign-wide reporting, make sure historical comments are matched to the specific entitlement, not just the identity and application account.

The key question is whether you need a snapshot of all challenge-related fields or a true chronological audit trail containing every decision change.

Let’s us know if it is help you ! :t_rex:

Hello Vijay. For this requirement in IIQ 8.4p4, I would suggest a custom LiveReport, since the standard reports do not provide the complete challenge and decision history.

Here is how I would approach it:

  1. Use CertificationItem as the datasource and filter by Certification Group to cover the campaign in one report.
  2. Retrieve the associated CertificationChallenge for each item to capture the challenger comment, Accept/Reject decision, and certifier comment.
  3. Retrieve the current decision from the item’s certification action. Make sure items without challenges are also included, and review any additional filters.

The important part is the original Revoke decision. As explained in the 8.4 documentation, accepting a challenge moves the item back to Open for another decision. So the current decision may no longer represent the original Revoke, although comments remain with the certification item.

I would suggest testing one item through Revoke → Challenge → Accept → New Decision, then checking whether you can retrieve each decision with its correct comment, actor, and timestamp.

If the original Revoke is missing, check the Certification History and underlying historical records. If those details are retained, you can extend the report to include them. This should give you a campaign-wide reporting approach. I would just verify the original decision history before treating it as a complete audit trail.

IIQ 8.4p4 do not provide the complete Challenge Period history - i have not checked all the reports but what i know there is no report whith these info.

also, as @sbisson told be carefull with decision history, CertificationItem.action contains only LAST decision. CertifivationChallenge contains info only about challenge. if you have 2 or more decision makers and signers then you need to be carefull because as already told iiq do not keep decision history automatically (out of the box) and in CertificationItem.action we have only LAST decision and info about it.

yu can create your own custom report, using certificationitem, certificationchallenge and other objects or you can modify existing report (which i wrote), it is java, but it is modifiable little.

information about challenge :

  • Challenge period start date –
  1. a. Based on the planned date for non-rolling certifications
    For non-rolling certifications (Certification.isUseRollingPhases() == false)
    the Challenge Period Start Date can be calculated based on the certification schedule. The calculation uses the certification start date and phase configuration:
    Certification.getCreated()
    Certification.getPhaseConfig()
    CertificationStateConfig.calculateStateEndDate(...)

    This calculates the end of the Active phase, because the Challenge phase starts immediately after the Active phase.
  2. b. Based on CertificationChallenge.created
    Another possible option is to use CertificationChallenge.getCreated().
    The challenge object is created after the certifier makes a “revoke” decision. But object is created only after “Performance Maintenance” task.
  • Challenger reply date –
    For this column, the reliable source is the work item. All other dates are replaced by next/other operations. When the challenger replies to the certifier’s decision, the challenge work item is completed and archived. This corresponds to the challenger’s reply date. This date can be obtained as WorkItemArchive.getArchived(). (we have not checked this. we have disabled.)

  • Challenger comment –
    This comment can be obtained directly from CertificationChallenge by getCompletionComments() method.

  • Certifier decision date –
    After certifier’s decision on the challenge, most probably the challenge object remains unchanged and the modify date can be used as certifier decision date.
    But first, the report should check that:
    • CertificationChallenge object exists
    • CertificationChallenge.isChallenged() is true and
    • CertificationChallenge.hasBeenDecidedUpon() is true
    This means:
    • the challenge object was created;
    • the user really challenged the decision;
    • the certifier has already made a decision on the challenge.
    After these checks, the date can be taken as CertificationChallenge.getModified().

    There is also CertificationItem.action.decisiondate which is date of last decision. We can get it if decision on challenge is Accept, because in case of Accept the old decision is erased and new one is created and date is updated. But in case of Reject, previous decision is kept and date is not changed and it indicates decision date of initial decision. In this case we can get like it was described above date of modify of challenge object.

  • Certifier decision on challenge –
    This can be obtained as CertificationChallenge.getDecision()

  • Certifier comment on challenge –
    The certifier’s comment on the challenge can be taken directly from CertificationChallenge.
    Method: CertificationChallenge.getDecisionComments()
    This returns the comment entered by the certifier when deciding on the challenge.

this is our investigations.

hope this will help understan topic.

Hi All,

Thank you for the detailed information and your support. This is very helpful and gives us a good direction to proceed with the custom LiveReport.

Really appreciate your guidance and support!